From 2e05ca4fa899e4e922562286767061b4ec9fe12b Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Thu, 8 Mar 2018 05:11:41 -0800 Subject: [PATCH] Fix #2513 for all element types --- CHANGELOG-v3.md | 8 +++ src/base/Element.php | 4 +- src/controllers/EntriesController.php | 7 --- src/db/Connection.php | 34 +++++++++++ src/helpers/StringHelper.php | 4 +- src/validators/StringValidator.php | 84 +++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 src/validators/StringValidator.php diff --git a/CHANGELOG-v3.md b/CHANGELOG-v3.md index b465499c5e1..3030ed489a5 100644 --- a/CHANGELOG-v3.md +++ b/CHANGELOG-v3.md @@ -2,6 +2,14 @@ ## Unreleased +### Added +- Added `craft\db\Connection::getSupportsMb4()` and `setSupportsMb4()`. +- Added `craft\helpers\StringHelper::containsMb4()`. +- Added `craft\validators\StringValidator`. + +### Changed +- Element titles now get a validation error if they contain any 4+ byte characters (like emoji), on servers running MySQL. ([#2513](https://github.com/craftcms/cms/issues/2513)) + ### Fixed - Fixed an error that occurred when creating a new entry draft. ([#2544](https://github.com/craftcms/cms/issues/2544)) - Fixed a bug where the primary action button on element index pages was getting positioned off-screen on IE11. ([#2545](https://github.com/craftcms/cms/issues/2545)) diff --git a/src/base/Element.php b/src/base/Element.php index 11cbd1265d6..2f01b7d5fc9 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -37,6 +37,7 @@ use craft\validators\ElementUriValidator; use craft\validators\SiteIdValidator; use craft\validators\SlugValidator; +use craft\validators\StringValidator; use craft\web\UploadedFile; use DateTime; use yii\base\Event; @@ -45,7 +46,6 @@ use yii\base\InvalidConfigException; use yii\base\UnknownPropertyException; use yii\validators\NumberValidator; -use yii\validators\StringValidator; use yii\validators\Validator; /** @@ -901,7 +901,7 @@ public function rules() ]; if (static::hasTitles()) { - $rules[] = [['title'], 'string', 'max' => 255, 'on' => [self::SCENARIO_DEFAULT, self::SCENARIO_LIVE]]; + $rules[] = [['title'], StringValidator::class, 'max' => 255, 'disallowMb4' => true, 'on' => [self::SCENARIO_DEFAULT, self::SCENARIO_LIVE]]; $rules[] = [['title'], 'required', 'on' => [self::SCENARIO_DEFAULT, self::SCENARIO_LIVE]]; } diff --git a/src/controllers/EntriesController.php b/src/controllers/EntriesController.php index 626fdf4eee2..c6572824bb5 100644 --- a/src/controllers/EntriesController.php +++ b/src/controllers/EntriesController.php @@ -532,13 +532,6 @@ public function actionSaveEntry() $revisionsService->saveVersion($currentEntry); } - // Validate that the title does not have an emoji - if (StringHelper::hasMb4($entry->title)) { - Craft::$app->getSession()->setError(Craft::t('app', 'Couldn’t save entry with emoji in title.')); - return null; - } - - // Save the entry (finally!) if ($entry->enabled && $entry->enabledForSite) { $entry->setScenario(Element::SCENARIO_LIVE); diff --git a/src/db/Connection.php b/src/db/Connection.php index 8374cb6ef73..0f216376ff5 100644 --- a/src/db/Connection.php +++ b/src/db/Connection.php @@ -30,6 +30,7 @@ * @inheritdoc * @property MysqlQueryBuilder|PgsqlQueryBuilder $queryBuilder The query builder for the current DB connection. * @property MysqlSchema|PgsqlSchema $schema The schema information for the database opened by this connection. + * @property bool $supportsMb4 Whether the database supports 4+ byte characters. * @method MysqlQueryBuilder|PgsqlQueryBuilder getQueryBuilder() Returns the query builder for the current DB connection. * @method MysqlSchema|PgsqlSchema getSchema() Returns the schema information for the database opened by this connection. * @method TableSchema getTableSchema($name, $refresh = false) Obtains the schema information for the named table. @@ -103,6 +104,16 @@ public static function createFromConfig(DbConfig $config): Connection ]); } + // Properties + // ========================================================================= + + /** + * @var bool|null whether the database supports 4+ byte characters + * @see getSupportsMb4() + * @see setSupportsMb4() + */ + private $_supportsMb4; + // Public Methods // ========================================================================= @@ -137,6 +148,29 @@ public function getVersion(): string return App::normalizeVersion($version); } + /** + * Returns whether the database supports 4+ byte characters. + * + * @return bool + */ + public function getSupportsMb4(): bool + { + if ($this->_supportsMb4 !== null) { + return $this->_supportsMb4; + } + return $this->_supportsMb4 = $this->getIsPgsql(); + } + + /** + * Sets whether the database supports 4+ byte characters. + * + * @param bool $supportsMb4 + */ + public function setSupportsMb4(bool $supportsMb4) + { + $this->_supportsMb4 = $supportsMb4; + } + /** * @inheritdoc * @throws DbConnectException if there are any issues diff --git a/src/helpers/StringHelper.php b/src/helpers/StringHelper.php index c53f64aeaf6..54c44d7ee24 100644 --- a/src/helpers/StringHelper.php +++ b/src/helpers/StringHelper.php @@ -971,7 +971,7 @@ public static function encoding(string $string): string * @param string $string * @return bool */ - public static function hasMb4(string $string): bool + public static function containsMb4(string $string): bool { return max(array_map('ord', str_split($string))) >= 240; } @@ -986,7 +986,7 @@ public static function hasMb4(string $string): bool public static function encodeMb4(string $string): string { // Does this string have any 4+ byte Unicode chars? - if (max(array_map('ord', str_split($string))) >= 240) { + if (static::containsMb4($string)) { $string = preg_replace_callback('/./u', function(array $match) { if (strlen($match[0]) >= 4) { // (Logic pulled from WP's wp_encode_emoji() function) diff --git a/src/validators/StringValidator.php b/src/validators/StringValidator.php new file mode 100644 index 00000000000..d41db8f7bd5 --- /dev/null +++ b/src/validators/StringValidator.php @@ -0,0 +1,84 @@ + + * @since 3.0 + */ +class StringValidator extends \yii\validators\StringValidator +{ + // Properties + // ========================================================================= + + /** + * @var bool whether the string should be checked for 4+ byte characters (like emoji) + */ + public $disallowMb4 = false; + + /** + * @var string user-defined error message used when the value contains 4+ byte characters + * (like emoji) and the database doesn’t support it. + */ + public $containsMb4; + + // Public Methods + // ========================================================================= + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + + if ($this->containsMb4 === null) { + $this->containsMb4 = Craft::t('app', '{attribute} cannot contain emoji.'); + } + } + + /** + * @inheritdoc + */ + public function validateAttribute($model, $attribute) + { + parent::validateAttribute($model, $attribute); + + $value = $model->$attribute; + if (!is_string($value)) { + return; + } + + if ($this->disallowMb4 && !Craft::$app->getDb()->getSupportsMb4() && StringHelper::containsMb4($value)) { + $this->addError($model, $attribute, $this->containsMb4); + } + } + + /** + * @inheritdoc + */ + public function validateValue($value) + { + if (!empty($result = parent::validateValue($value))) { + return $result; + } + + if ($this->disallowMb4 && !Craft::$app->getDb()->getSupportsMb4() && StringHelper::containsMb4($value)) { + return [$this->containsMb4, []]; + } + + return null; + } +}