diff --git a/vendor/magento/module-catalog/Ui/DataProvider/Product/Form/Modifier/Eav/WysiwygConfigDataProcessor.php b/vendor/magento/module-catalog/Ui/DataProvider/Product/Form/Modifier/Eav/WysiwygConfigDataProcessor.php index d301a45d14ff5..e3c4e8a5b5029 100644 --- a/vendor/magento/module-catalog/Ui/DataProvider/Product/Form/Modifier/Eav/WysiwygConfigDataProcessor.php +++ b/vendor/magento/module-catalog/Ui/DataProvider/Product/Form/Modifier/Eav/WysiwygConfigDataProcessor.php @@ -14,16 +14,28 @@ class WysiwygConfigDataProcessor implements WysiwygConfigDataProcessorInterface { /** - * {@inheritdoc} + * Build WYSIWYG config data for a product attribute. + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @return array */ public function process(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) { - return [ + $wysiwygConfigData = [ 'add_variables' => false, 'add_widgets' => false, 'add_directives' => true, 'use_container' => true, 'container_class' => 'hor-scroll', ]; + + if ((string)$attribute->getBackendType() === 'text' && (string)$attribute->getBackendTable() !== '') { + $wysiwygConfigData['utf8mb4Target'] = [ + 'table' => (string)$attribute->getBackendTable(), + 'column' => 'value', + ]; + } + + return $wysiwygConfigData; } } diff --git a/vendor/magento/module-catalog/view/adminhtml/ui_component/category_form.xml b/vendor/magento/module-catalog/view/adminhtml/ui_component/category_form.xml index 02366e74fb089..81dc09ddf4261 100644 --- a/vendor/magento/module-catalog/view/adminhtml/ui_component/category_form.xml +++ b/vendor/magento/module-catalog/view/adminhtml/ui_component/category_form.xml @@ -197,6 +197,10 @@ true true + + catalog_category_entity_text + value + category diff --git a/vendor/magento/module-cms/view/adminhtml/ui_component/cms_block_form.xml b/vendor/magento/module-cms/view/adminhtml/ui_component/cms_block_form.xml index fd81373c23c85..cbeed28ddf576 100644 --- a/vendor/magento/module-cms/view/adminhtml/ui_component/cms_block_form.xml +++ b/vendor/magento/module-cms/view/adminhtml/ui_component/cms_block_form.xml @@ -143,6 +143,10 @@ block + + cms_block + content + diff --git a/vendor/magento/module-cms/view/adminhtml/ui_component/cms_page_form.xml b/vendor/magento/module-cms/view/adminhtml/ui_component/cms_page_form.xml index 6f531b4fda86c..b768ab85f5213 100644 --- a/vendor/magento/module-cms/view/adminhtml/ui_component/cms_page_form.xml +++ b/vendor/magento/module-cms/view/adminhtml/ui_component/cms_page_form.xml @@ -118,6 +118,10 @@ page + + cms_page + content + diff --git a/vendor/magento/module-ui/Component/Form/Element/Wysiwyg.php b/vendor/magento/module-ui/Component/Form/Element/Wysiwyg.php index 040c27d4939ef..419727cd080de 100644 --- a/vendor/magento/module-ui/Component/Form/Element/Wysiwyg.php +++ b/vendor/magento/module-ui/Component/Form/Element/Wysiwyg.php @@ -6,11 +6,13 @@ namespace Magento\Ui\Component\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\Editor; use Magento\Framework\Data\Form; use Magento\Framework\Data\FormFactory; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Wysiwyg\ConfigInterface; +use Magento\Ui\Model\Validation\WysiwygValidationConfigResolver; /** * WYSIWYG form element @@ -20,7 +22,7 @@ */ class Wysiwyg extends AbstractElement { - const NAME = 'wysiwyg'; + public const NAME = 'wysiwyg'; /** * @var Form @@ -34,6 +36,11 @@ class Wysiwyg extends AbstractElement */ protected $editor; + /** + * @var WysiwygValidationConfigResolver + */ + private WysiwygValidationConfigResolver $validationConfigResolver; + /** * @param ContextInterface $context * @param FormFactory $formFactory @@ -41,6 +48,7 @@ class Wysiwyg extends AbstractElement * @param array $components * @param array $data * @param array $config + * @param WysiwygValidationConfigResolver|null $validationConfigResolver */ public function __construct( ContextInterface $context, @@ -48,12 +56,16 @@ public function __construct( ConfigInterface $wysiwygConfig, array $components = [], array $data = [], - array $config = [] + array $config = [], + ?WysiwygValidationConfigResolver $validationConfigResolver = null ) { + $this->validationConfigResolver = $validationConfigResolver + ?? ObjectManager::getInstance()->get(WysiwygValidationConfigResolver::class); $wysiwygConfigData = isset($config['wysiwygConfigData']) ? $config['wysiwygConfigData'] : []; - $this->form = $formFactory->create(); $wysiwygId = $context->getNamespace() . '_' . $data['name']; + $data['config']['validationParams']['allowUtf8mb4'] = $this->validationConfigResolver + ->resolveAllowUtf8mb4($config); $this->editor = $this->form->addField( $wysiwygId, \Magento\Framework\Data\Form\Element\Editor::class, diff --git a/vendor/magento/module-ui/Model/ResourceModel/Utf8mb4Support.php b/vendor/magento/module-ui/Model/ResourceModel/Utf8mb4Support.php new file mode 100644 index 0000000000000..88c4af86a8fc3 --- /dev/null +++ b/vendor/magento/module-ui/Model/ResourceModel/Utf8mb4Support.php @@ -0,0 +1,108 @@ + + */ + private array $columnSupportCache = []; + + /** + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function isColumnSupported(string $table, string $column): bool + { + $cacheKey = $table . '.' . $column; + + if (array_key_exists($cacheKey, $this->columnSupportCache)) { + return $this->columnSupportCache[$cacheKey]; + } + + try { + $connection = $this->resourceConnection->getConnection(); + + $this->columnSupportCache[$cacheKey] = $this->isConnectionUtf8mb4($connection) + && $this->isColumnCharsetUtf8mb4($connection, $table, $column); + } catch (\Throwable $exception) { + $this->logger->warning($exception->getMessage()); + $this->columnSupportCache[$cacheKey] = false; + } + + return $this->columnSupportCache[$cacheKey]; + } + + /** + * Check if connection is set to utf8mb4. + * + * @param AdapterInterface $connection + * @return bool + */ + private function isConnectionUtf8mb4(AdapterInterface $connection): bool + { + $row = $connection->fetchRow( + 'SELECT @@character_set_connection AS charset, @@collation_connection AS collation' + ); + + return is_array($row) + && isset($row['charset'], $row['collation']) + && str_starts_with((string)$row['charset'], 'utf8mb4') + && str_starts_with((string)$row['collation'], 'utf8mb4'); + } + + /** + * Check if column to be updated supports utf8mb4 character set. + * + * @param AdapterInterface $connection + * @param string $table + * @param string $column + * @return bool + */ + private function isColumnCharsetUtf8mb4(AdapterInterface $connection, string $table, string $column): bool + { + $row = $connection->fetchRow( + sprintf( + 'SHOW FULL COLUMNS FROM `%s` LIKE %s', + $this->resourceConnection->getTableName($table), + $connection->quote($column) + ) + ); + + // Text/blob columns without collation are not safe for utf8mb4 round-tripping. + return is_array($row) + && !empty($row['Collation']) + && str_starts_with((string)$row['Collation'], 'utf8mb4'); + } +} diff --git a/vendor/magento/module-ui/Model/ResourceModel/Utf8mb4SupportInterface.php b/vendor/magento/module-ui/Model/ResourceModel/Utf8mb4SupportInterface.php new file mode 100644 index 0000000000000..de51eae06ce10 --- /dev/null +++ b/vendor/magento/module-ui/Model/ResourceModel/Utf8mb4SupportInterface.php @@ -0,0 +1,20 @@ + + */ + private array $supportCache = []; + + /** + * @param Utf8mb4SupportInterface $utf8mb4Support + */ + public function __construct(Utf8mb4SupportInterface $utf8mb4Support) + { + $this->utf8mb4Support = $utf8mb4Support; + } + + /** + * Resolve whether utf8mb4 should be allowed for the current field. + * + * @param array $config + * @return bool + */ + public function resolveAllowUtf8mb4(array $config): bool + { + $allowUtf8mb4 = $config['allowUtf8mb4'] ?? $config['wysiwygConfigData']['allowUtf8mb4'] ?? null; + + if (is_bool($allowUtf8mb4)) { + return $allowUtf8mb4; + } + + $target = $this->resolveTarget($config); + + if ($target === null) { + return false; + } + + $cacheKey = $target['table'] . '.' . $target['column']; + + if (!array_key_exists($cacheKey, $this->supportCache)) { + $this->supportCache[$cacheKey] = $this->utf8mb4Support->isColumnSupported( + $target['table'], + $target['column'] + ); + } + + return $this->supportCache[$cacheKey]; + } + + /** + * Resolve the storage target for the current field. + * + * @param array $config + * @return array{table: string, column: string}|null + */ + private function resolveTarget(array $config): ?array + { + $target = $config['utf8mb4Target'] ?? $config['wysiwygConfigData']['utf8mb4Target'] ?? null; + + if (!is_array($target)) { + return null; + } + + $table = is_string($target['table'] ?? null) ? $target['table'] : ''; + $column = is_string($target['column'] ?? null) ? $target['column'] : ''; + + if ($table === '' || $column === '') { + return null; + } + + return [ + 'table' => $table, + 'column' => $column, + ]; + } +} diff --git a/vendor/magento/module-ui/etc/di.xml b/vendor/magento/module-ui/etc/di.xml index 65253ab8279d4..a32afe1f6eb7f 100644 --- a/vendor/magento/module-ui/etc/di.xml +++ b/vendor/magento/module-ui/etc/di.xml @@ -17,6 +17,7 @@ + diff --git a/vendor/magento/module-ui/view/base/ui_component/etc/definition.xml b/vendor/magento/module-ui/view/base/ui_component/etc/definition.xml index 40d42d834b004..b7ed48245d99d 100644 --- a/vendor/magento/module-ui/view/base/ui_component/etc/definition.xml +++ b/vendor/magento/module-ui/view/base/ui_component/etc/definition.xml @@ -126,7 +126,7 @@ ui/content/content - true + true diff --git a/vendor/magento/module-ui/view/base/web/js/lib/validation/rules.js b/vendor/magento/module-ui/view/base/web/js/lib/validation/rules.js index 0272acf2a9561..4c41696f396fa 100644 --- a/vendor/magento/module-ui/view/base/web/js/lib/validation/rules.js +++ b/vendor/magento/module-ui/view/base/web/js/lib/validation/rules.js @@ -18,6 +18,61 @@ define([ ], function ($, _, utils, moment, tinycolor) { 'use strict'; + function validateNoInvalidUnicodeCharacters(value, validator) { + var message = $.mage.__('Please remove invalid characters: {0}.'), + matches = [], + uniqueMatches = [], + seenMatches = {}, + index, + code, + nextCode; + + for (index = 0; index < value.length; index++) { + code = value.charCodeAt(index); + + if (code >= 0xD800 && code <= 0xDBFF) { + nextCode = value.charCodeAt(index + 1); + + // eslint-disable-next-line max-depth + if (!(nextCode >= 0xDC00 && nextCode <= 0xDFFF)) { + matches.push('U+' + code.toString(16).toUpperCase()); + } else { + index++; + } + } else if (code >= 0xDC00 && code <= 0xDFFF) { + matches.push('U+' + code.toString(16).toUpperCase()); + } + } + + if (matches.length) { + matches.forEach(function (match) { + if (!seenMatches[match]) { + seenMatches[match] = true; + uniqueMatches.push(match); + } + }); + + validator.charErrorMessage = message.replace('{0}', uniqueMatches.join(', ')); + + return false; + } + + return true; + } + + function validateNoUtf8mb4Characters(value, validator) { + var message = $.mage.__('Please remove invalid characters: {0}.'), + matches = value.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF])/g); + + if (matches !== null) { + validator.charErrorMessage = message.replace('{0}', matches.join()); + + return false; + } + + return true; + } + /** * validate credit card number using mod10 * @param {String} s @@ -1104,18 +1159,33 @@ define([ }, $.mage.__('The Date of Birth should not be greater than today.') ], - '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; + 'validate-wysiwyg-content-characters': [ + function (value, params, additionalParams) { + var allowUtf8mb4 = additionalParams && + additionalParams.allowUtf8mb4 === true; - if (!result) { - validator.charErrorMessage = message.replace('{0}', matches.join()); + if (!allowUtf8mb4) { + allowUtf8mb4 = window.wysiwygValidationConfig && + window.wysiwygValidationConfig.allowUtf8mb4 === true; } - return result; + return allowUtf8mb4 ? + validateNoInvalidUnicodeCharacters(value, this) : + validateNoUtf8mb4Characters(value, this); + }, function () { + return this.charErrorMessage; + } + ], + 'validate-no-invalid-unicode-characters': [ + function (value) { + return validateNoInvalidUnicodeCharacters(value, this); + }, function () { + return this.charErrorMessage; + } + ], + 'validate-no-utf8mb4-characters': [ + function (value) { + return validateNoUtf8mb4Characters(value, this); }, function () { return this.charErrorMessage; } diff --git a/vendor/magento/magento2-base/lib/web/mage/validation.js b/vendor/magento/magento2-base/lib/web/mage/validation.js index 72baf69740c9b..63045a40dbf40 100644 --- a/vendor/magento/magento2-base/lib/web/mage/validation.js +++ b/vendor/magento/magento2-base/lib/web/mage/validation.js @@ -177,6 +177,62 @@ define([ return c % 10 === 0; } + /* eslint-disable max-depth */ + function validateNoInvalidUnicodeCharacters(value, validator) { + var message = $.mage.__('Please remove invalid characters: {0}.'), + matches = [], + uniqueMatches = [], + seenMatches = {}, + index, + code, + nextCode; + + for (index = 0; index < value.length; index++) { + code = value.charCodeAt(index); + + if (code >= 0xD800 && code <= 0xDBFF) { + nextCode = value.charCodeAt(index + 1); + + if (!(nextCode >= 0xDC00 && nextCode <= 0xDFFF)) { + matches.push('U+' + code.toString(16).toUpperCase()); + } else { + index++; + } + } else if (code >= 0xDC00 && code <= 0xDFFF) { + matches.push('U+' + code.toString(16).toUpperCase()); + } + } + + if (matches.length) { + matches.forEach(function (match) { + if (!seenMatches[match]) { + seenMatches[match] = true; + uniqueMatches.push(match); + } + }); + + validator.charErrorMessage = message.replace('{0}', uniqueMatches.join(', ')); + + return false; + } + + return true; + } + /* eslint-enable max-depth */ + + function validateNoUtf8mb4Characters(value, validator) { + var message = $.mage.__('Please remove invalid characters: {0}.'), + matches = value.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF])/g); + + if (matches !== null) { + validator.charErrorMessage = message.replace('{0}', matches.join()); + + return false; + } + + return true; + } + /** * validate all table required inputs at once, using single hidden input * @param {String} value @@ -423,20 +479,35 @@ define([ }, $.mage.__('Please enter at least {0} characters') ], + 'validate-wysiwyg-content-characters': [ + function (value, element, params) { + var allowUtf8mb4 = params && + params.allowUtf8mb4 === true; + + if (!allowUtf8mb4) { + allowUtf8mb4 = window.wysiwygValidationConfig && + window.wysiwygValidationConfig.allowUtf8mb4 === true; + } + + return allowUtf8mb4 ? + validateNoInvalidUnicodeCharacters(value, this) : + validateNoUtf8mb4Characters(value, this); + }, function () { + return this.charErrorMessage; + } + ], + 'validate-no-invalid-unicode-characters': [ + function (value) { + return validateNoInvalidUnicodeCharacters(value, this); + }, function () { + return this.charErrorMessage; + } + ], /* 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; + return validateNoUtf8mb4Characters(value, this); }, function () { return this.charErrorMessage; }