diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index fbdda684343b583da89fa7f75869d9d5297e8cdb..32c1c1b6d7a610068ec5b13d4219a318929cb756 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -81,8 +81,9 @@ class Attributes extends \Magento\Framework\View\Element\Template $attributes = $product->getAttributes(); foreach ($attributes as $attribute) { if ($attribute->getIsVisibleOnFront() && !in_array($attribute->getAttributeCode(), $excludeAttr)) { - $value = $attribute->getFrontend()->getValue($product); - + if (is_array($value = $attribute->getFrontend()->getValue($product))) { + continue; + } if (!$product->hasData($attribute->getAttributeCode())) { $value = __('N/A'); } elseif ((string)$value == '') { @@ -90,7 +91,6 @@ class Attributes extends \Magento\Framework\View\Element\Template } elseif ($attribute->getFrontendInput() == 'price' && is_string($value)) { $value = $this->priceCurrency->convertAndFormat($value); } - if ($value instanceof Phrase || (is_string($value) && strlen($value))) { $data[$attribute->getAttributeCode()] = [ 'label' => __($attribute->getStoreLabel()), diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml index a0041d2e02988122430ba1558f4fdb1523d9f6a9..6ff0e193a774f4b1878a289518a971e698ea3347 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/options.phtml @@ -88,7 +88,9 @@ $stores = $block->getStoresSortedBySortOrder(); $values = []; foreach($block->getOptionValues() as $value) { $value = $value->getData(); - $values[] = is_array($value) ? array_map("htmlspecialchars_decode", $value) : $value; + $values[] = is_array($value) ? array_map(function($str) { + return htmlspecialchars_decode($str, ENT_QUOTES); + }, $value) : $value; } ?> <script type="text/x-magento-init"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml index 79dc8591fd724f62e3cf57dadcd6e29e9a798e02..11aedc33c2d423a932b8339695663544cbbe55a1 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml @@ -29,6 +29,7 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; if ($_option->getMaxCharacters()) { $_textValidate['maxlength'] = $_option->getMaxCharacters(); } + $_textValidate['validate-no-utf8mb4-characters'] = true; ?> <input type="text" id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" @@ -47,6 +48,7 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; if ($_option->getMaxCharacters()) { $_textAreaValidate['maxlength'] = $_option->getMaxCharacters(); } + $_textAreaValidate['validate-no-utf8mb4-characters'] = true; ?> <textarea id="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text" class="product-custom-option" diff --git a/app/code/Magento/Theme/Model/Design/Config/Storage.php b/app/code/Magento/Theme/Model/Design/Config/Storage.php index a73f70efa0adf7f20716b7d8e73577f174590f6c..c97114f963a09bbf29458264de8e9d0534313c5a 100644 --- a/app/code/Magento/Theme/Model/Design/Config/Storage.php +++ b/app/code/Magento/Theme/Model/Design/Config/Storage.php @@ -87,10 +87,13 @@ class Storage $scopeId, $fieldData->getFieldConfig() ); - if ($value !== null) { - $fieldData->setValue($value); + + if ($value === null) { + $value = ''; } + $fieldData->setValue($value); } + return $designConfig; } diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/Design/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/Design/ConfigTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0ff43a5fd41cae96de8a7a226c435a1057ff695e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/Design/ConfigTest.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Theme\Model\Design; + +/** + * Test for \Magento\Theme\Model\Design\Config\Storage. + */ +class ConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Theme\Model\Design\Config\Storage + */ + private $storage; + + protected function setUp() + { + $this->storage = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Theme\Model\Design\Config\Storage::class + ); + } + + /** + * Test design/header/welcome if it is saved in db as empty(null) it should be shown on backend as empty. + * + * @magentoDataFixture Magento/Theme/_files/config_data.php + */ + public function testLoad() + { + $data = $this->storage->load('stores', 1); + foreach ($data->getExtensionAttributes()->getDesignConfigData() as $configData) { + if ($configData->getPath() == 'design/header/welcome') { + $this->assertSame('', $configData->getValue()); + } + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Theme/_files/config_data.php b/dev/tests/integration/testsuite/Magento/Theme/_files/config_data.php new file mode 100644 index 0000000000000000000000000000000000000000..b8cbebc1f67c1a4409981fc0c68e368c43f81de9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Theme/_files/config_data.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Config\Model\Config\Factory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Factory $configFactory */ +$configFactory = $objectManager->create(Factory::class); +/** @var \Magento\Config\Model\Config $config */ +$config = $configFactory->create(); +$config->setScope('stores'); +$config->setStore('default'); +$config->setDataByPath('design/header/welcome', null); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Theme/_files/config_data_rollback.php b/dev/tests/integration/testsuite/Magento/Theme/_files/config_data_rollback.php new file mode 100644 index 0000000000000000000000000000000000000000..ac02e98b493731ad0e9e927319fa581535ac752d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Theme/_files/config_data_rollback.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Framework\App\ResourceConnection $resource */ +$resource = $objectManager->get(\Magento\Framework\App\ResourceConnection::class); +$connection = $resource->getConnection(); +$tableName = $resource->getTableName('core_config_data'); + +$connection->query("DELETE FROM $tableName WHERE path = 'design/header/welcome';"); diff --git a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js index 50931f940c689e5450cc872f4be2fc19530583ef..12138e5939a7b1bdc487f3a27f7e003930287c48 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js @@ -183,4 +183,76 @@ define([ )).toEqual(true); }); }); + + describe('Testing 3 bytes characters only policy (UTF-8)', function () { + it('rejects data, if any of the characters cannot be stored using UTF-8 collation', function () { + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '😅😂', null + )).toEqual(false); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '😅 test 😂', null + )).toEqual(false); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '💩 👻 💀', null + )).toEqual(false); + }); + + it('approves data, if all the characters can be stored using UTF-8 collation', function () { + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '!$-_%ç&#?!', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '1234567890', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, ' ', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'test', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'иÑпытание', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'теÑÑ‚', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'ÖƒÕ¸Ö€Õ±Õ¡Ö€Õ¯Õ¸Ö‚Õ´', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'परीकà¥à¤·à¤£', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'テスト', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '테스트', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '测试', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, '測試', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'ทดสà¸à¸š', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'δοκιμή', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'اختبار', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'تست', null + )).toEqual(true); + expect($.validator.methods['validate-no-utf8mb4-characters'].call( + $.validator.prototype, 'מִבְחָן', null + )).toEqual(true); + }); + }); + }); diff --git a/lib/web/i18n/en_US.csv b/lib/web/i18n/en_US.csv index 21cfb51d5e3c9ba0ed08853ba7de38486f070bf6..5c63a191420a4d2daeebffef9dcab9e886edbd93 100644 --- a/lib/web/i18n/en_US.csv +++ b/lib/web/i18n/en_US.csv @@ -99,6 +99,7 @@ Submit,Submit "Password cannot be the same as email address.","Password cannot be the same as email address." "Please fix this field.","Please fix this field." "Please enter a valid email address.","Please enter a valid email address." +"Please remove invalid characters: {0}.", "Please remove invalid characters: {0}." "Please enter a valid URL.","Please enter a valid URL." "Please enter a valid date (ISO).","Please enter a valid date (ISO)." "Please enter only digits.","Please enter only digits." diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 85158c581aec175b50525e5dbfe59ae58cc5c678..fee88826be7eb76adaa906667f48279c64f5452a 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -396,6 +396,24 @@ $.mage.__('Please enter at least {0} characters') ], + /* detect chars that would require more than 3 bytes */ + 'validate-no-utf8mb4-characters': [ + function (value) { + var validator = this, + message = $.mage.__('Please remove invalid characters: {0}.'), + matches = value.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF])/g), + result = matches === null; + + if (!result) { + validator.charErrorMessage = message.replace('{0}', matches.join()); + } + + return result; + }, function () { + return this.charErrorMessage; + } + ], + /* eslint-disable max-len */ 'email2': [ function (value, element) {