diff --git a/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/info.phtml b/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/info.phtml index f60bbe670a3e3..25dd6cca9f8eb 100644 --- a/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/info.phtml +++ b/app/code/Magento/Authorizenet/view/adminhtml/templates/directpost/info.phtml @@ -40,9 +40,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); 'validate-cc-type-select':'#_cc_number' }"> - getCcAvailableTypes() as $typeCode => $typeName) : ?> + getCcAvailableTypes() as $typeCode => $typeName): ?> @@ -57,6 +57,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year');
- getCcMonths() as $k => $v) : ?> + getCcMonths() as $k => $v): ?> @@ -92,9 +95,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); class="admin__control-select admin__control-select-year" data-container="-cc-year" data-validate="{required:true}"> - getCcYears() as $k => $v) : ?> + getCcYears() as $k => $v): ?> @@ -102,7 +105,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year');
- hasVerification()) : ?> + hasVerification()): ?>
@@ -47,18 +54,18 @@ $ccExpYear = $block->getInfoData('cc_exp_year');
+ name="payment[cc_cid]" + value="getInfoData('cc_cid') ?>"/>
diff --git a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml index 88e0d5edc2a7d..4d2d60cfb2473 100644 --- a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml +++ b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml @@ -8,8 +8,11 @@ /** @var \Magento\Captcha\Model\DefaultModel $captcha */ $captcha = $block->getCaptchaModel(); +$name = $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); +/** @var bool $validationEnabled */ +$validationEnabled = $block->hasData('frontend_validation') ? $block->getData('frontend_validation') : true; ?> -
+
@@ -18,11 +21,13 @@ $captcha = $block->getCaptchaModel(); id="captcha" class="admin__control-text" type="text" - name="escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) ?>[escapeHtml($block->getFormId()) ?>]" - data-validate="{required:true}"/> - isCaseSensitive()) :?> + name="[escapeHtmlAttr($block->getFormId()) ?>]" + data-validate="{required:true}"/> + isCaseSensitive()): ?>
- escapeHtml(__('Attention: Captcha is case sensitive.'), ['strong']) ?> + + escapeHtml(__('Attention: Captcha is case sensitive.'), ['strong']) ?> +
@@ -43,7 +48,10 @@ $captcha = $block->getCaptchaModel(); require(["prototype", "mage/captcha"], function(){ //escapeJs($block->escapeUrl($block->getRefreshUrl())) ?>', 'escapeJs($block->escapeHtml($block->getFormId())) ?>'); + var captcha = new Captcha( + 'escapeJs($block->getRefreshUrl()) ?>', + 'escapeJs($block->getFormId()) ?>' + ); $('captcha-reload').observe('click', function () { captcha.refresh(this); diff --git a/app/code/Magento/Captcha/view/frontend/templates/default.phtml b/app/code/Magento/Captcha/view/frontend/templates/default.phtml index ead8c590eee94..0d436cc62b790 100644 --- a/app/code/Magento/Captcha/view/frontend/templates/default.phtml +++ b/app/code/Magento/Captcha/view/frontend/templates/default.phtml @@ -8,27 +8,46 @@ /** @var \Magento\Captcha\Model\DefaultModel $captcha */ $captcha = $block->getCaptchaModel(); +/** @var bool $validationEnabled */ +$validationEnabled = $block->hasData('frontend_validation') ? $block->getData('frontend_validation') : true; +$inputName = $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); +$loaderUrl = $block->escapeUrl($block->getViewFileUrl('images/loader-2.gif')); +$note = $block->escapeHtml(__('Attention: Captcha is case sensitive.'), ['strong']); ?> -
- +
+
- + data-validate="{required:true}" + id="captcha_escapeHtmlAttr($block->getFormId()) ?>" + autocomplete="off"/>
", + "imageLoader": "", "type": "escapeHtmlAttr($block->getFormId()) ?>"}}'>
- <?= $block->escapeHtmlAttr(__('Please type the letters and numbers below')) ?> - + <?= $block->escapeHtmlAttr(__('Please type the letters and numbers below')) ?> +
- isCaseSensitive()) :?> -
- escapeHtml(__('Attention: Captcha is case sensitive.'), ['strong']) ?> -
+ isCaseSensitive()):?> +
diff --git a/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php b/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php index e398bf400391b..018c18112a763 100644 --- a/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php +++ b/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php @@ -11,7 +11,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * Thrown when too many payment processing requests have been initiated by a user. + * Thrown when too many payment processing/saving requests have been initiated by a user. */ class PaymentProcessingRateLimitExceededException extends LocalizedException { diff --git a/app/code/Magento/Checkout/Api/PaymentSavingRateLimiterInterface.php b/app/code/Magento/Checkout/Api/PaymentSavingRateLimiterInterface.php new file mode 100644 index 0000000000000..c964029ff8b3a --- /dev/null +++ b/app/code/Magento/Checkout/Api/PaymentSavingRateLimiterInterface.php @@ -0,0 +1,25 @@ + false, 'after' => '-', 'form_id' => 'sales_rule_coupon_request', - 'image_width' => 230, - 'image_height' => 230 + 'img_width' => 230, + 'img_height' => 50 ] ); } diff --git a/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php b/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php index 6f71423acbcaa..68860b629c246 100644 --- a/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php +++ b/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php @@ -12,42 +12,23 @@ use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Captcha\Model\DefaultModel as Captcha; use Magento\Captcha\Helper\Data as CaptchaHelper; use Magento\Captcha\Observer\CaptchaStringResolver as CaptchaResolver; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; /** - * Utilize CAPTCHA as a rate-limiting mechanism. + * Utilize CAPTCHA to limit payment processing requests. */ class CaptchaPaymentProcessingRateLimiter implements PaymentProcessingRateLimiterInterface { public const CAPTCHA_FORM = 'payment_processing_request'; /** - * @var UserContextInterface + * @var CaptchaRateLimiter */ - private $userContext; - - /** - * @var CustomerRepositoryInterface - */ - private $customerRepo; - - /** - * @var CaptchaHelper - */ - private $captchaHelper; - - /** - * @var RequestInterface - */ - private $request; - - /** - * @var CaptchaResolver - */ - private $captchaResolver; + private $limiter; /** * CaptchaPaymentProcessingRateLimiter constructor. @@ -57,19 +38,25 @@ class CaptchaPaymentProcessingRateLimiter implements PaymentProcessingRateLimite * @param CaptchaHelper $captchaHelper * @param RequestInterface $request * @param CaptchaResolver $captchaResolver + * @param CaptchaRateLimiterFactory|null $limiterFactory */ public function __construct( UserContextInterface $userContext, CustomerRepositoryInterface $customerRepo, CaptchaHelper $captchaHelper, RequestInterface $request, - CaptchaResolver $captchaResolver + CaptchaResolver $captchaResolver, + ?CaptchaRateLimiterFactory $limiterFactory ) { - $this->userContext = $userContext; - $this->customerRepo = $customerRepo; - $this->captchaHelper = $captchaHelper; - $this->request = $request; - $this->captchaResolver = $captchaResolver; + $limiterFactory = $limiterFactory ?? ObjectManager::getInstance()->get(CaptchaRateLimiterFactory::class); + $this->limiter = $limiterFactory->create([ + 'userContext' => $userContext, + 'customerRepo' => $customerRepo, + 'captchaHelper' => $captchaHelper, + 'captchaResolver' => $captchaResolver, + 'request' => $request, + 'captchaId' => self::CAPTCHA_FORM + ]); } /** @@ -77,47 +64,10 @@ public function __construct( */ public function limit(): void { - if ($this->userContext->getUserType() !== UserContextInterface::USER_TYPE_GUEST - && $this->userContext->getUserType() !== UserContextInterface::USER_TYPE_CUSTOMER - && $this->userContext->getUserType() !== null - ) { - return; + try { + $this->limiter->limit(); + } catch (LocalizedException $exception) { + throw new PaymentProcessingRateLimitExceededException(__($exception->getMessage()), $exception); } - - $login = $this->retrieveLogin(); - /** @var Captcha $captcha */ - $captcha = $this->captchaHelper->getCaptcha(self::CAPTCHA_FORM); - /** @var PaymentProcessingRateLimitExceededException|null $exception */ - $exception = null; - if ($captcha->isRequired($login)) { - $value = $this->captchaResolver->resolve($this->request, self::CAPTCHA_FORM); - if ($value && !$captcha->isCorrect($value)) { - $exception = new PaymentProcessingRateLimitExceededException(__('Incorrect CAPTCHA')); - } elseif (!$value) { - $exception = new PaymentProcessingRateLimitExceededException( - __('Please provide CAPTCHA code and try again') - ); - } - } - - $captcha->logAttempt($login); - if ($exception) { - throw $exception; - } - } - - /** - * Retrieve current user login. - * - * @return string|null - */ - private function retrieveLogin(): ?string - { - $login = null; - if ($this->userContext->getUserId()) { - $login = $this->customerRepo->getById($this->userContext->getUserId())->getEmail(); - } - - return $login; } } diff --git a/app/code/Magento/Checkout/Model/CaptchaPaymentSavingRateLimiter.php b/app/code/Magento/Checkout/Model/CaptchaPaymentSavingRateLimiter.php new file mode 100644 index 0000000000000..7d52a1a0e19a0 --- /dev/null +++ b/app/code/Magento/Checkout/Model/CaptchaPaymentSavingRateLimiter.php @@ -0,0 +1,55 @@ +limiter = $limiterFactory->create(['captchaId' => self::CAPTCHA_FORM]); + } + + /** + * @inheritDoc + */ + public function limit(): void + { + try { + $this->limiter->limit(); + } catch (LocalizedException $exception) { + throw new PaymentProcessingRateLimitExceededException( + __( + 'Could not store billing/shipping information at the moment' + .' but you can proceed with the checkout' + ), + $exception + ); + } + } +} diff --git a/app/code/Magento/Checkout/Model/CaptchaRateLimiter.php b/app/code/Magento/Checkout/Model/CaptchaRateLimiter.php new file mode 100644 index 0000000000000..e643d61e82b18 --- /dev/null +++ b/app/code/Magento/Checkout/Model/CaptchaRateLimiter.php @@ -0,0 +1,129 @@ +userContext = $userContext; + $this->customerRepo = $customerRepo; + $this->captchaHelper = $captchaHelper; + $this->request = $request; + $this->captchaResolver = $captchaResolver; + $this->captchaId = $captchaId; + } + + /** + * Validate CAPTCHA if necessary. + * + * @return void + * @throws LocalizedException + */ + public function limit(): void + { + if ($this->userContext->getUserType() !== UserContextInterface::USER_TYPE_GUEST + && $this->userContext->getUserType() !== UserContextInterface::USER_TYPE_CUSTOMER + && $this->userContext->getUserType() !== null + ) { + return; + } + + $login = $this->retrieveLogin(); + /** @var Captcha $captcha */ + $captcha = $this->captchaHelper->getCaptcha($this->captchaId); + /** @var LocalizedException|null $exception */ + $exception = null; + if ($captcha->isRequired($login)) { + $value = $this->captchaResolver->resolve($this->request, $this->captchaId); + if ($value && !$captcha->isCorrect($value)) { + $exception = new LocalizedException(__('Incorrect CAPTCHA')); + } elseif (!$value) { + $exception = new LocalizedException( + __('Please provide CAPTCHA code and try again') + ); + } + } + + $captcha->logAttempt($login); + if ($exception) { + throw $exception; + } + } + + /** + * Retrieve current user login. + * + * @return string|null + */ + private function retrieveLogin(): ?string + { + $login = null; + if ($this->userContext->getUserId()) { + $login = $this->customerRepo->getById($this->userContext->getUserId())->getEmail(); + } + + return $login; + } +} diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 2b2824213df79..0ca398b73a08b 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -7,7 +7,9 @@ namespace Magento\Checkout\Model; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Checkout\Api\PaymentSavingRateLimiterInterface; use Magento\Framework\App\ObjectManager; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; @@ -61,6 +63,16 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa */ private $paymentsRateLimiter; + /** + * @var PaymentSavingRateLimiterInterface + */ + private $savingRateLimiter; + + /** + * @var bool + */ + private $saveRateLimitDisabled = false; + /** * @param \Magento\Quote\Api\GuestBillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\GuestPaymentMethodManagementInterface $paymentMethodManagement @@ -69,6 +81,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository * @param PaymentProcessingRateLimiterInterface|null $paymentsRateLimiter + * @param PaymentSavingRateLimiterInterface|null $savingRateLimiter * @codeCoverageIgnore */ public function __construct( @@ -78,7 +91,8 @@ public function __construct( \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement, \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory, CartRepositoryInterface $cartRepository, - ?PaymentProcessingRateLimiterInterface $paymentsRateLimiter = null + ?PaymentProcessingRateLimiterInterface $paymentsRateLimiter = null, + ?PaymentSavingRateLimiterInterface $savingRateLimiter = null ) { $this->billingAddressManagement = $billingAddressManagement; $this->paymentMethodManagement = $paymentMethodManagement; @@ -88,6 +102,8 @@ public function __construct( $this->cartRepository = $cartRepository; $this->paymentsRateLimiter = $paymentsRateLimiter ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); + $this->savingRateLimiter = $savingRateLimiter + ?? ObjectManager::getInstance()->get(PaymentSavingRateLimiterInterface::class); } /** @@ -99,7 +115,14 @@ public function savePaymentInformationAndPlaceOrder( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { - $this->savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); + $this->paymentsRateLimiter->limit(); + try { + //Have to do this hack because of savePaymentInformation() plugins. + $this->saveRateLimitDisabled = true; + $this->savePaymentInformation($cartId, $email, $paymentMethod, $billingAddress); + } finally { + $this->saveRateLimitDisabled = false; + } try { $orderId = $this->cartManagement->placeOrder($cartId); } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -130,7 +153,14 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { - $this->paymentsRateLimiter->limit(); + if (!$this->saveRateLimitDisabled) { + try { + $this->savingRateLimiter->limit(); + } catch (PaymentProcessingRateLimitExceededException $ex) { + //Limit reached + return false; + } + } $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); /** @var Quote $quote */ diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index a6e448ecdb87e..3b36391530c8f 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -6,7 +6,9 @@ namespace Magento\Checkout\Model; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Checkout\Api\PaymentSavingRateLimiterInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotSaveException; @@ -58,6 +60,16 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor */ private $paymentRateLimiter; + /** + * @var PaymentSavingRateLimiterInterface + */ + private $saveRateLimiter; + + /** + * @var bool + */ + private $saveRateLimiterDisabled = false; + /** * @param \Magento\Quote\Api\BillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement @@ -65,6 +77,7 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor * @param PaymentDetailsFactory $paymentDetailsFactory * @param \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalsRepository * @param PaymentProcessingRateLimiterInterface|null $paymentRateLimiter + * @param PaymentSavingRateLimiterInterface|null $saveRateLimiter * @codeCoverageIgnore */ public function __construct( @@ -73,7 +86,8 @@ public function __construct( \Magento\Quote\Api\CartManagementInterface $cartManagement, \Magento\Checkout\Model\PaymentDetailsFactory $paymentDetailsFactory, \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalsRepository, - ?PaymentProcessingRateLimiterInterface $paymentRateLimiter = null + ?PaymentProcessingRateLimiterInterface $paymentRateLimiter = null, + ?PaymentSavingRateLimiterInterface $saveRateLimiter = null ) { $this->billingAddressManagement = $billingAddressManagement; $this->paymentMethodManagement = $paymentMethodManagement; @@ -82,6 +96,8 @@ public function __construct( $this->cartTotalsRepository = $cartTotalsRepository; $this->paymentRateLimiter = $paymentRateLimiter ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); + $this->saveRateLimiter = $saveRateLimiter + ?? ObjectManager::getInstance()->get(PaymentSavingRateLimiterInterface::class); } /** @@ -92,7 +108,14 @@ public function savePaymentInformationAndPlaceOrder( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { - $this->savePaymentInformation($cartId, $paymentMethod, $billingAddress); + $this->paymentRateLimiter->limit(); + try { + //Have to do this hack because of plugins for savePaymentInformation() + $this->saveRateLimiterDisabled = true; + $this->savePaymentInformation($cartId, $paymentMethod, $billingAddress); + } finally { + $this->saveRateLimiterDisabled = false; + } try { $orderId = $this->cartManagement->placeOrder($cartId); } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -121,7 +144,14 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { - $this->paymentRateLimiter->limit(); + if (!$this->saveRateLimiterDisabled) { + try { + $this->saveRateLimiter->limit(); + } catch (PaymentProcessingRateLimitExceededException $ex) { + //Limit reached + return false; + } + } if ($billingAddress) { /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index d62d092e7a75e..ad000135c6bde 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -85,6 +85,7 @@ + diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index a8fd14cc433fa..53f33cd67f3cb 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -9,6 +9,7 @@ use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Checkout\Api\PaymentSavingRateLimiterInterface; use Magento\Checkout\Model\GuestPaymentInformationManagement; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\LocalizedException; @@ -73,6 +74,11 @@ class GuestPaymentInformationManagementTest extends TestCase */ private $limiterMock; + /** + * @var PaymentSavingRateLimiterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $saveLimiterMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -91,6 +97,7 @@ protected function setUp(): void ); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->limiterMock = $this->getMockForAbstractClass(PaymentProcessingRateLimiterInterface::class); + $this->saveLimiterMock = $this->getMockForAbstractClass(PaymentSavingRateLimiterInterface::class); $this->model = $objectManager->getObject( GuestPaymentInformationManagement::class, [ @@ -99,7 +106,8 @@ protected function setUp(): void 'cartManagement' => $this->cartManagementMock, 'cartRepository' => $this->cartRepositoryMock, 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, - 'paymentsRateLimiter' => $this->limiterMock + 'paymentsRateLimiter' => $this->limiterMock, + 'savingRateLimiter' => $this->saveLimiterMock ] ); $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); @@ -161,11 +169,10 @@ public function testSavePaymentInformation() */ public function testSavePaymentInformationLimited(): void { - $this->expectException(PaymentProcessingRateLimitExceededException::class); - $this->limiterMock->method('limit') + $this->saveLimiterMock->method('limit') ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); - $this->savePayment(); + $this->assertFalse($this->savePayment()); } public function testSavePaymentInformationWithoutBillingAddress() diff --git a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php index d437974450e6f..d8a09a7072794 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php @@ -9,6 +9,7 @@ use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Checkout\Api\PaymentSavingRateLimiterInterface; use Magento\Checkout\Model\PaymentInformationManagement; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Phrase; @@ -61,10 +62,15 @@ class PaymentInformationManagementTest extends TestCase private $cartRepositoryMock; /** - * @var PaymentProcessingRateLimiterInterface|MockObject + * @var PaymentProcessingRateLimiterInterface|\PHPUnit_Framework_MockObject_MockObject */ private $rateLimiterMock; + /** + * @var PaymentSavingRateLimiterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $saveLimiterMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -80,13 +86,15 @@ protected function setUp(): void $this->cartRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) ->getMock(); $this->rateLimiterMock = $this->getMockForAbstractClass(PaymentProcessingRateLimiterInterface::class); + $this->saveLimiterMock = $this->getMockForAbstractClass(PaymentSavingRateLimiterInterface::class); $this->model = $objectManager->getObject( PaymentInformationManagement::class, [ 'billingAddressManagement' => $this->billingAddressManagementMock, 'paymentMethodManagement' => $this->paymentMethodManagementMock, 'cartManagement' => $this->cartManagementMock, - 'paymentRateLimiter' => $this->rateLimiterMock + 'paymentRateLimiter' => $this->rateLimiterMock, + 'saveRateLimiter' => $this->saveLimiterMock ] ); $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); @@ -166,11 +174,10 @@ public function testSavePaymentInformation() */ public function testSavePaymentInformationLimited(): void { - $this->rateLimiterMock->method('limit') + $this->saveLimiterMock->method('limit') ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); - $this->expectException(PaymentProcessingRateLimitExceededException::class); - $this->savePayment(); + $this->assertFalse($this->savePayment()); } public function testSavePaymentInformationWithoutBillingAddress() diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 0c1d866dfc2fb..1af46c878694b 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -51,4 +51,6 @@ + diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js index ae5b0914e83a6..94d4907395528 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js @@ -15,7 +15,7 @@ define([ 'Magento_Checkout/js/action/get-totals', 'Magento_Checkout/js/model/full-screen-loader', 'underscore', - 'Magento_Checkout/js/model/payment/set-payment-hooks' + 'Magento_Checkout/js/model/payment/place-order-hooks' ], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader, _, hooks) { 'use strict'; @@ -79,9 +79,6 @@ define([ ).always( function () { fullScreenLoader.stopLoader(); - _.each(hooks.afterRequestListeners, function (listener) { - listener(); - }); } ); }; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js deleted file mode 100644 index 5cd31d85c9a29..0000000000000 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([], function () { - 'use strict'; - - return { - requestModifiers: [], - afterRequestListeners: [] - }; -}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js b/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js deleted file mode 100644 index 93f3bb8b2a45c..0000000000000 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'Magento_Captcha/js/view/checkout/defaultCaptcha', - 'Magento_Captcha/js/model/captchaList', - 'underscore', - 'Magento_Checkout/js/model/payment/set-payment-hooks' -], -function (defaultCaptcha, captchaList, _, setPaymentHooks) { - 'use strict'; - - return defaultCaptcha.extend({ - /** @inheritdoc */ - initialize: function () { - var self = this, - currentCaptcha; - - this._super(); - currentCaptcha = captchaList.getCaptchaByFormId(this.formId); - - if (currentCaptcha != null) { - currentCaptcha.setIsVisible(true); - this.setCurrentCaptcha(currentCaptcha); - setPaymentHooks.requestModifiers.push(function (headers) { - if (self.isRequired()) { - headers['X-Captcha'] = self.captchaValue()(); - } - }); - setPaymentHooks.afterRequestListeners.push(function () { - self.refresh(); - }); - } - } - }); -}); diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 05da219b67b92..0a0b348973f01 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -127,8 +127,9 @@ protected function _prepareLayout() 'cacheable' => false, 'after' => '-', 'form_id' => CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM, - 'image_width' => 230, - 'image_height' => 230 + 'img_width' => 230, + 'img_height' => 50, + 'frontend_validation' => false ] ); } diff --git a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php index b3333d828a094..734251f18455d 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php @@ -13,6 +13,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Exception\PaymentException; use Magento\Framework\Session\SessionManagerInterface; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; /** * Placing orders. @@ -126,6 +127,9 @@ public function execute() $this->_getCheckout()->getCheckoutSession()->setDisplaySuccess(true); $this->_redirect('*/*/success'); } + } catch (PaymentProcessingRateLimitExceededException $ex) { + $this->messageManager->addErrorMessage($ex->getMessage()); + $this->_redirect('*/*/overview'); } catch (PaymentException $e) { $message = $e->getMessage(); if (!empty($message)) { diff --git a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml index 4b7da0f9b7862..b122da755b873 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml @@ -22,8 +22,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year');
@@ -47,18 +53,18 @@ $ccExpYear = $block->getInfoData('cc_exp_year');
+ name="payment[cc_cid]" + value="getInfoData('cc_cid') ?>"/>
diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml index 0c1df4e239810..81666132184c3 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml @@ -31,7 +31,9 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); "orderSaveUrl":"escapeUrl($block->getOrderUrl()) ?>", "cgiUrl":"escapeUrl($block->getCgiUrl()) ?>", "expireYearLength":"escapeHtml($block->getMethodConfigData('cc_year_length')) ?>", - "nativeAction":"escapeUrl($block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()])) ?>" + "nativeAction":"escapeUrl( + $block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" }, "validation":[]}' style="display: none;">
@@ -43,12 +45,16 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); - getCcMonths() as $k => $v) : ?> + getCcMonths() as $k => $v): ?> @@ -98,29 +108,37 @@ $ccExpMonth = $block->getInfoData('cc_exp_month');
- hasVerification()) : ?> + hasVerification()): ?>
-
diff --git a/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml b/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml index 3a0a0e864a45e..22e7dfbba6aca 100644 --- a/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml @@ -29,9 +29,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); }' class="select"> - getCcAvailableTypes() as $typeCode => $typeName) : ?> + getCcAvailableTypes() as $typeCode => $typeName): ?> @@ -43,8 +43,15 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); escapeHtml(__('Credit Card Number')) ?>
-
- + getCcMonths() as $k => $v): ?> @@ -75,9 +88,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year');
+ getViewFileUrl('Magento_Checkout::cvv.png') . '\" alt=\"' . $block->escapeHtml(__('Card Verification Number Visual Reference')) . '\" title=\"' . $block->escapeHtml(__('Card Verification Number Visual Reference')) . '\" />'; ?> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml index 0cf8ae8afd260..d49e7cb269731 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml @@ -27,7 +27,9 @@ $content = 'escapeUrl($block->getViewFileUrl('Magento_Che "cgiUrl":"escapeUrl($block->getCgiUrl()) ?>", "dateDelim":"escapeHtml($block->getDateDelim()) ?>", "cardFieldsMap":escapeHtml($block->getCardFieldsMap()) ?>, - "nativeAction":"escapeUrl($block->getUrl('checkout/onepage/saveOrder', ['_secure' => $block->getRequest()->isSecure()])) ?>" + "nativeAction":"escapeUrl( + $block->getUrl('checkout/onepage/saveOrder', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" }, "validation":[]}'>
@@ -45,9 +47,9 @@ $content = 'escapeUrl($block->getViewFileUrl('Magento_Che "validate-cc-type-select":"#_cc_number" }'> - getCcAvailableTypes() as $typeCode => $typeName) : ?> + getCcAvailableTypes() as $typeCode => $typeName): ?> @@ -59,8 +61,14 @@ $content = 'escapeUrl($block->getViewFileUrl('Magento_Che
escapeUrl($block->getViewFileUrl('Magento_Che required:true, "validate-cc-exp":"#_expiration_yr" }'> - getCcMonths() as $k => $v) : ?> + getCcMonths() as $k => $v): ?> @@ -97,9 +105,9 @@ $content = 'escapeUrl($block->getViewFileUrl('Magento_Che '; }, + /** + * Get image for CVV for Unsanitized Html + * @returns {String} + */ + getCvvImageUnsanitizedHtml: function () { + return this.getCvvImageHtml(); + }, + /** * @deprecated * @returns {Object} diff --git a/app/code/Magento/Payment/view/frontend/web/template/payment/cc-form.html b/app/code/Magento/Payment/view/frontend/web/template/payment/cc-form.html index bb23014cda834..e60e3aae11799 100644 --- a/app/code/Magento/Payment/view/frontend/web/template/payment/cc-form.html +++ b/app/code/Magento/Payment/view/frontend/web/template/payment/cc-form.html @@ -44,7 +44,13 @@
-
+ data-bind="html: getCvvImageUnsanitizedHtml()">
diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml index ae42803a26dd3..1b54d5a76fdd8 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml @@ -31,7 +31,9 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); "orderSaveUrl":"escapeUrl($block->getOrderUrl()) ?>", "cgiUrl":"escapeUrl($block->getCgiUrl()) ?>", "expireYearLength":"escapeHtml($block->getMethodConfigData('cc_year_length')) ?>", - "nativeAction":"escapeUrl($block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()])) ?>" + "nativeAction":"escapeUrl( + $block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" }, "validation":[]}' style="display: none;">
@@ -46,9 +48,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-validate='{required:true, "validate-cc-type-select":"#_cc_number"}' class="admin__control-select"> - getCcAvailableTypes() as $typeCode => $typeName) : ?> + getCcAvailableTypes() as $typeCode => $typeName): ?> @@ -64,6 +67,9 @@ $ccExpMonth = $block->getInfoData('cc_exp_month');
getInfoData('cc_exp_month'); data-container="-cc-month" class="admin__control-select admin__control-select-month" data-validate='{required:true, "validate-cc-exp":"#_expiration_yr"}'> - getCcMonths() as $k => $v) : ?> + getCcMonths() as $k => $v): ?> @@ -98,17 +104,17 @@ $ccExpMonth = $block->getInfoData('cc_exp_month');
- hasVerification()) : ?> + hasVerification()): ?>
- isVaultEnabled()) : ?> + isVaultEnabled()): ?>
paymentMethodManagement = $paymentMethodManagement; $this->paymentFactory = $paymentFactory; $this->additionalDataProviderPool = $additionalDataProviderPool; - $this->paymentRateLimiter = $paymentRateLimiter - ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); + $this->paymentRateLimiter = $savingRateLimiter + ?? ObjectManager::getInstance()->get(PaymentSavingRateLimiterInterface::class); } /** @@ -77,7 +82,12 @@ public function __construct( public function execute(Quote $cart, array $paymentData): void { try { - $this->paymentRateLimiter->limit(); + try { + $this->paymentRateLimiter->limit(); + } catch (PaymentProcessingRateLimitExceededException $ex) { + //Limit reached + return; + } } catch (PaymentProcessingRateLimitExceededException $exception) { throw new GraphQlInputException(__($exception->getMessage()), $exception); } diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php index 281e1233d8bbe..45bd3538fbcb1 100644 --- a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php @@ -9,7 +9,7 @@ namespace Magento\QuoteGraphQl\Test\Unit\Model\Cart; use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; -use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Checkout\Api\PaymentSavingRateLimiterInterface; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\Quote; @@ -25,7 +25,7 @@ class SetPaymentMethodOnCartTest extends TestCase private $model; /** - * @var PaymentProcessingRateLimiterInterface|MockObject + * @var PaymentSavingRateLimiterInterface|MockObject */ private $rateLimiterMock; @@ -37,10 +37,10 @@ protected function setUp(): void parent::setUp(); $objectManager = new ObjectManager($this); - $this->rateLimiterMock = $this->getMockForAbstractClass(PaymentProcessingRateLimiterInterface::class); + $this->rateLimiterMock = $this->getMockForAbstractClass(PaymentSavingRateLimiterInterface::class); $this->model = $objectManager->getObject( SetPaymentMethodOnCart::class, - ['paymentRateLimiter' => $this->rateLimiterMock] + ['savingRateLimiter' => $this->rateLimiterMock] ); } @@ -52,10 +52,9 @@ protected function setUp(): void public function testLimited(): void { $this->rateLimiterMock->method('limit') - ->willThrowException(new PaymentProcessingRateLimitExceededException(__($message = 'Error'))); - $this->expectException(GraphQlInputException::class); - $this->expectExceptionMessage($message); + ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); + //There will be en error if the limiter won't stop the execution $this->model->execute($this->createMock(Quote::class), []); } } diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 6f2154ba29f47..04fe39542d135 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -240,10 +240,12 @@ protected function _prepareLayout() 'cacheable' => false, 'after' => '-', 'form_id' => 'product_sendtofriend_form', - 'image_width' => 230, - 'image_height' => 230 + 'img_width' => 230, + 'img_height' => 50 ] ); } + + return $this; } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaRateLimiterTest.php similarity index 90% rename from dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php rename to dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaRateLimiterTest.php index 2a7b3c29223be..1f858a4d474ad 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaRateLimiterTest.php @@ -9,9 +9,9 @@ namespace Magento\Checkout\Model; use Magento\Captcha\Model\DefaultModel; -use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -27,10 +27,10 @@ * @magentoAppIsolation enabled * @magentoAppArea frontend */ -class CaptchaPaymentProcessingRateLimiterTest extends TestCase +class CaptchaRateLimiterTest extends TestCase { /** - * @var CaptchaPaymentProcessingRateLimiter + * @var CaptchaRateLimiter */ private $model; @@ -69,7 +69,10 @@ protected function setUp(): void $this->captchaHelper = $objectManager->get(CaptchaHelper::class); $this->customerSession = $objectManager->get(CustomerSession::class); $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); - $this->model = $objectManager->get(CaptchaPaymentProcessingRateLimiter::class); + $this->model = $objectManager->create( + CaptchaRateLimiter::class, + ['captchaId' => 'payment_processing_request'] + ); } /** @@ -95,7 +98,7 @@ public function testLoggedInLimits(): void try { $this->model->limit(); $limited = false; - } catch (PaymentProcessingRateLimitExceededException $exception) { + } catch (LocalizedException $exception) { $limited = true; } $this->assertTrue($limited); @@ -118,7 +121,7 @@ public function testGuestLimits(): void try { $this->model->limit(); $limited = false; - } catch (PaymentProcessingRateLimitExceededException $exception) { + } catch (LocalizedException $exception) { $limited = true; } $this->assertTrue($limited); @@ -141,7 +144,7 @@ public function testCaptchaValidation(): void try { $this->model->limit(); $limited = false; - } catch (PaymentProcessingRateLimitExceededException $exception) { + } catch (LocalizedException $exception) { $limited = true; } //CAPTCHA is required @@ -176,7 +179,7 @@ public function testCaptchaValidation(): void try { $this->model->limit(); $limited = false; - } catch (PaymentProcessingRateLimitExceededException $exception) { + } catch (LocalizedException $exception) { $limited = true; } //CAPTCHA was validated diff --git a/lib/internal/Magento/Framework/App/StaticResource.php b/lib/internal/Magento/Framework/App/StaticResource.php index 2a4aa3cd4be4b..c39a86752eefe 100644 --- a/lib/internal/Magento/Framework/App/StaticResource.php +++ b/lib/internal/Magento/Framework/App/StaticResource.php @@ -9,6 +9,8 @@ use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\Filesystem; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Validator\Locale; +use Magento\Framework\View\Design\Theme\ThemePackageList; use Psr\Log\LoggerInterface; use Magento\Framework\Debug; use Magento\Framework\Filesystem\Driver\File; @@ -80,6 +82,16 @@ class StaticResource implements \Magento\Framework\AppInterface */ private $driver; + /** + * @var ThemePackageList + */ + private $themePackageList; + + /** + * @var Locale + */ + private $localeValidator; + /** * @param State $state * @param Response\FileInterface $response @@ -91,6 +103,8 @@ class StaticResource implements \Magento\Framework\AppInterface * @param ConfigLoaderInterface $configLoader * @param DeploymentConfig|null $deploymentConfig * @param File|null $driver + * @param ThemePackageList|null $themePackageList + * @param Locale|null $localeValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -104,7 +118,9 @@ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, ConfigLoaderInterface $configLoader, DeploymentConfig $deploymentConfig = null, - File $driver = null + File $driver = null, + ThemePackageList $themePackageList = null, + Locale $localeValidator = null ) { $this->state = $state; $this->response = $response; @@ -116,6 +132,8 @@ public function __construct( $this->configLoader = $configLoader; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->driver = $driver ?: ObjectManager::getInstance()->get(File::class); + $this->themePackageList = $themePackageList ?? ObjectManager::getInstance()->get(ThemePackageList::class); + $this->localeValidator = $localeValidator ?? ObjectManager::getInstance()->get(Locale::class); } /** @@ -138,6 +156,16 @@ public function launch() } else { $path = $this->request->get('resource'); $params = $this->parsePath($path); + if (!($this->isThemeAllowed($params['area'] . DIRECTORY_SEPARATOR . $params['theme']) + && $this->localeValidator->isValid($params['locale'])) + ) { + if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION) { + $this->response->setHttpResponseCode(404); + return $this->response; + } + throw new \InvalidArgumentException('Requested path ' . $path . ' is wrong.'); + } + $this->state->setAreaCode($params['area']); $this->objectManager->configure($this->configLoader->load($params['area'])); $file = $params['file']; @@ -236,4 +264,15 @@ private function getLogger() return $this->logger; } + + /** + * Check that theme is available. + * + * @param string $theme + * @return bool + */ + private function isThemeAllowed(string $theme): bool + { + return in_array($theme, array_keys($this->themePackageList->getThemes())); + } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php b/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php index e74b424b4941e..abc6643bf7f2b 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php @@ -21,6 +21,8 @@ use Psr\Log\LoggerInterface; use PHPUnit\Framework\MockObject\MockObject as MockObject; use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\View\Design\Theme\ThemePackageList; +use Magento\Framework\Validator\Locale; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -77,6 +79,16 @@ class StaticResourceTest extends \PHPUnit\Framework\TestCase */ private $deploymentConfigMock; + /** + * @var ThemePackageList|MockObject + */ + private $themePackageListMock; + + /** + * @var Locale|MockObject + */ + private $localeValidatorMock; + /** * @var StaticResource */ @@ -100,6 +112,8 @@ protected function setUp(): void $this->configLoaderMock = $this->createMock(ConfigLoader::class); $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); $this->driverMock = $this->createMock(File::class); + $this->themePackageListMock = $this->createMock(ThemePackageList::class); + $this->localeValidatorMock = $this->createMock(Locale::class); $this->object = new StaticResource( $this->stateMock, $this->responseMock, @@ -110,7 +124,9 @@ protected function setUp(): void $this->objectManagerMock, $this->configLoaderMock, $this->deploymentConfigMock, - $this->driverMock + $this->driverMock, + $this->themePackageListMock, + $this->localeValidatorMock ); } @@ -201,6 +217,17 @@ public function testLaunch( $this->driverMock->expects($this->once()) ->method('getRealPathSafety') ->willReturnArgument(0); + $this->themePackageListMock->expects($this->atLeastOnce())->method('getThemes')->willReturn( + [ + 'area/Magento/theme' => [ + 'area' => 'area', + 'vendor' => 'Magento', + 'name' => 'theme', + ], + ] + ); + $this->localeValidatorMock->expects($this->once())->method('isValid')->willReturn(true); + $this->object->launch(); } @@ -322,4 +349,86 @@ public function testLaunchPathAbove() $this->object->launch(); } + + /** + * @param array $themes + * @dataProvider themesDataProvider + */ + public function testLaunchWithInvalidTheme(array $themes): void + { + $this->expectException('InvalidArgumentException'); + $path = 'frontend/Test/luma/en_US/calendar.css'; + + $this->stateMock->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + $this->requestMock->expects($this->once()) + ->method('get') + ->with('resource') + ->willReturn($path); + $this->driverMock->expects($this->once()) + ->method('getRealPathSafety') + ->with($path) + ->willReturn($path); + $this->themePackageListMock->expects($this->once())->method('getThemes')->willReturn($themes); + $this->localeValidatorMock->expects($this->never())->method('isValid'); + $this->expectExceptionMessage('Requested path ' . $path . ' is wrong.'); + + $this->object->launch(); + } + + /** + * @param array $themes + * @dataProvider themesDataProvider + */ + public function testLaunchWithInvalidLocale(array $themes): void + { + $this->expectException('InvalidArgumentException'); + $path = 'frontend/Magento/luma/test/calendar.css'; + + $this->stateMock->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + $this->requestMock->expects($this->once()) + ->method('get') + ->with('resource') + ->willReturn($path); + $this->driverMock->expects($this->once()) + ->method('getRealPathSafety') + ->with($path) + ->willReturn($path); + $this->themePackageListMock->expects($this->once())->method('getThemes')->willReturn($themes); + $this->localeValidatorMock->expects($this->once())->method('isValid')->willReturn(false); + $this->expectExceptionMessage('Requested path ' . $path . ' is wrong.'); + + $this->object->launch(); + } + + /** + * @return array + */ + public function themesDataProvider(): array + { + return [ + [ + [ + 'adminhtml/Magento/backend' => [ + 'area' => 'adminhtml', + 'vendor' => 'Magento', + 'name' => 'backend', + ], + 'frontend/Magento/blank' => [ + 'area' => 'frontend', + 'vendor' => 'Magento', + 'name' => 'blank', + ], + 'frontend/Magento/luma' => [ + 'area' => 'frontend', + 'vendor' => 'Magento', + 'name' => 'luma', + ], + ], + ], + ]; + } }