diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml index 13ced1c0263e0..64f365217d7e4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -14,5 +14,6 @@ + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index f31e8342ead23..40bdcff6ec937 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -87,9 +87,8 @@ - - - + + diff --git a/app/code/Magento/PageCache/Test/Mftf/ActionGroup/AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup.xml b/app/code/Magento/PageCache/Test/Mftf/ActionGroup/AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup.xml new file mode 100644 index 0000000000000..6ef5f878023a1 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/ActionGroup/AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup.xml @@ -0,0 +1,32 @@ + + + + + + Assert that product page add to cart form key is different from cached value. + + + + + + + + /\w{16}/ + {{cachedValue}} + + + /\w{16}/ + grabUpdatedValue + + + {{cachedValue}} + grabUpdatedValue + + + diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/StorefrontCachedInputFormKeyValueUpdatedTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/StorefrontCachedInputFormKeyValueUpdatedTest.xml new file mode 100644 index 0000000000000..a9d77429e3248 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/StorefrontCachedInputFormKeyValueUpdatedTest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + <description value="Form Key value should be updated by js script"/> + <testCaseId value="MC-39300"/> + <useCaseId value="MC-30171"/> + <severity value="AVERAGE"/> + <group value="pageCache"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="full_page"/> + </actionGroup> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <grabValueFrom selector="{{StorefrontProductActionSection.inputFormKey}}" stepKey="grabCachedValue"/> + <resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/> + <resetCookie userInput="form_key" stepKey="resetFormKeyCookie"/> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="reopenProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup" stepKey="assertValueIsUpdatedByScript"> + <argument name="cachedValue" value="{$grabCachedValue}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/PageCache/ViewModel/FormKeyProvider.php b/app/code/Magento/PageCache/ViewModel/FormKeyProvider.php new file mode 100644 index 0000000000000..26f6be43c627a --- /dev/null +++ b/app/code/Magento/PageCache/ViewModel/FormKeyProvider.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\ViewModel; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\PageCache\Model\Config; + +/** + * Adds script to update form key from cookie after script rendering + */ +class FormKeyProvider implements ArgumentInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @param Config $config + */ + public function __construct( + Config $config + ) { + $this->config = $config; + } + + /** + * Is full page cache enabled + * + * @return bool + */ + public function isFullPageCacheEnabled(): bool + { + return $this->config->isEnabled(); + } +} diff --git a/app/code/Magento/PageCache/view/frontend/layout/default.xml b/app/code/Magento/PageCache/view/frontend/layout/default.xml index 7e1fc9d31b864..3db4b1c4ae52e 100644 --- a/app/code/Magento/PageCache/view/frontend/layout/default.xml +++ b/app/code/Magento/PageCache/view/frontend/layout/default.xml @@ -10,6 +10,13 @@ <referenceBlock name="head.components"> <block class="Magento\Framework\View\Element\Js\Components" name="pagecache_page_head_components" template="Magento_PageCache::js/components.phtml"/> </referenceBlock> + <referenceBlock name="head.additional"> + <block name="form_key_provider" template="Magento_PageCache::form_key_provider.phtml"> + <arguments> + <argument name="form_key_provider" xsi:type="object">Magento\PageCache\ViewModel\FormKeyProvider</argument> + </arguments> + </block> + </referenceBlock> <referenceContainer name="content"> <block class="Magento\PageCache\Block\Javascript" template="Magento_PageCache::javascript.phtml" name="pageCache" as="pageCache"/> </referenceContainer> diff --git a/app/code/Magento/PageCache/view/frontend/requirejs-config.js b/app/code/Magento/PageCache/view/frontend/requirejs-config.js index 7a33e2748b916..59d4499092965 100644 --- a/app/code/Magento/PageCache/view/frontend/requirejs-config.js +++ b/app/code/Magento/PageCache/view/frontend/requirejs-config.js @@ -8,5 +8,6 @@ var config = { '*': { pageCache: 'Magento_PageCache/js/page-cache' } - } + }, + deps: ['Magento_PageCache/js/form-key-provider'] }; diff --git a/app/code/Magento/PageCache/view/frontend/templates/form_key_provider.phtml b/app/code/Magento/PageCache/view/frontend/templates/form_key_provider.phtml new file mode 100644 index 0000000000000..4f952002e458f --- /dev/null +++ b/app/code/Magento/PageCache/view/frontend/templates/form_key_provider.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +if ($block->getFormKeyProvider()->isFullPageCacheEnabled()): ?> + <script type="text/x-magento-init"> + { + "*": { + "Magento_PageCache/js/form-key-provider": {} + } + } + </script> +<?php endif; ?> diff --git a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js new file mode 100644 index 0000000000000..c63d97840e946 --- /dev/null +++ b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js @@ -0,0 +1,93 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define(function () { + 'use strict'; + + return function () { + var formKey, + inputElements, + inputSelector = 'input[name="form_key"]'; + + /** + * Set form_key cookie + * @private + */ + function setFormKeyCookie(value) { + var expires, + secure, + date = new Date(), + isSecure = !!window.cookiesConfig && window.cookiesConfig.secure; + + date.setTime(date.getTime() + 86400000); + expires = '; expires=' + date.toUTCString(); + secure = isSecure ? '; secure' : ''; + + document.cookie = 'form_key=' + (value || '') + expires + secure + '; path=/'; + } + + /** + * Retrieves form key from cookie + * @private + */ + function getFormKeyCookie() { + var cookie, + i, + nameEQ = 'form_key=', + cookieArr = document.cookie.split(';'); + + for (i = 0; i < cookieArr.length; i++) { + cookie = cookieArr[i]; + + while (cookie.charAt(0) === ' ') { + cookie = cookie.substring(1, cookie.length); + } + + if (cookie.indexOf(nameEQ) === 0) { + return cookie.substring(nameEQ.length, cookie.length); + } + } + + return null; + } + + /** + * Generate form key string + * @private + */ + function generateFormKeyString() { + var result = '', + length = 16, + chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + while (length--) { + result += chars[Math.round(Math.random() * (chars.length - 1))]; + } + + return result; + } + + /** + * Init form_key inputs with value + * @private + */ + function initFormKey() { + formKey = getFormKeyCookie(); + + if (!formKey) { + formKey = generateFormKeyString(); + setFormKeyCookie(formKey); + } + inputElements = document.querySelectorAll(inputSelector); + + if (inputElements.length) { + Array.prototype.forEach.call(inputElements, function (element) { + element.setAttribute('value', formKey); + }); + } + } + + initFormKey(); + }; +}); diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js index 41a32ab8a49c8..d7214918c530d 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js @@ -7,9 +7,10 @@ define([ 'jquery', 'domReady', 'consoleLogger', + 'Magento_PageCache/js/form-key-provider', 'jquery-ui-modules/widget', 'mage/cookies' -], function ($, domReady, consoleLogger) { +], function ($, domReady, consoleLogger, formKeyInit) { 'use strict'; /** @@ -99,6 +100,7 @@ define([ /** * FormKey Widget - this widget is generating from key, saves it to cookie and + * @deprecated see Magento/PageCache/view/frontend/web/js/form-key-provider.js */ $.widget('mage.formKey', { options: { @@ -298,8 +300,7 @@ define([ }); domReady(function () { - $('body') - .formKey(); + formKeyInit(); }); return { diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/form-key-provider.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/form-key-provider.test.js new file mode 100644 index 0000000000000..162c00a9c0cca --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/form-key-provider.test.js @@ -0,0 +1,46 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +define([ + 'jquery', + 'Magento_PageCache/js/form-key-provider' +], function ($, formKeyInit) { + 'use strict'; + + describe('Testing FormKey Provider', function () { + var inputContainer; + + beforeEach(function () { + inputContainer = document.createElement('input'); + inputContainer.setAttribute('value', ''); + inputContainer.setAttribute('name', 'form_key'); + document.querySelector('body').appendChild(inputContainer); + }); + + afterEach(function () { + $(inputContainer).remove(); + document.cookie = 'form_key= ; expires = Thu, 01 Jan 1970 00:00:00 GMT'; + }); + + it('sets value of input[form_key]', function () { + var expires, + date = new Date(); + + date.setTime(date.getTime() + 86400000); + expires = '; expires=' + date.toUTCString(); + document.cookie = 'form_key=FAKE_COOKIE' + expires + '; path=/'; + formKeyInit(); + expect($(inputContainer).val()).toEqual('FAKE_COOKIE'); + }); + + it('widget sets value to input[form_key] in case it empty', function () { + document.cookie = 'form_key= ; expires = Thu, 01 Jan 1970 00:00:00 GMT'; + formKeyInit(); + expect($(inputContainer).val()).toEqual(jasmine.any(String)); + expect($(inputContainer).val().length).toEqual(16); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/page-cache.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/page-cache.test.js index 14e0523fd5151..f12d36e888f22 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/page-cache.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/PageCache/frontend/js/page-cache.test.js @@ -106,13 +106,6 @@ define([ expect($.mage.cookies.set).toHaveBeenCalled(); expect(inputContainer.val()).toEqual(jasmine.any(String)); }); - - it('widget exists on load on body', function (done) { - $(function () { - expect($('body').data('mageFormKey')).toBeDefined(); - done(); - }); - }); }); describe('Testing PageCache Widget', function () {