From b105158ab75a593a59d26b10897f10fa1e7e5f1e Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Tue, 2 Jun 2015 10:13:37 +0100 Subject: [PATCH] Issue #1314214 by stefan.r, phayes, ergophobe, YesCT, damienwhaley, Tor Arne Thune, kbasarab, pfrenssen, basic, yannickoo, simolokid: MySQL driver does not support full UTF-8 (emojis, asian symbols, mathematical symbols) --- .../Core/Database/Driver/mysql/Connection.php | 10 ++-- .../Core/Database/Driver/mysql/Schema.php | 6 +-- .../aggregator/src/FeedStorageSchema.php | 2 +- .../src/BlockContentStorageSchema.php | 52 ------------------- .../block_content/src/Entity/BlockContent.php | 1 - core/modules/file/src/Entity/File.php | 3 +- core/modules/file/src/FileStorageSchema.php | 2 +- .../Validation/Constraint/FileUriUnique.php | 31 +++++++++++ core/modules/file/src/Tests/SaveTest.php | 18 ++++++- .../migrate/src/Plugin/migrate/id_map/Sql.php | 6 +++ .../src/Tests/Table/d6/System.php | 4 +- core/modules/node/src/Tests/NodeViewTest.php | 12 +++++ .../src/Tests/Database/RegressionTest.php | 10 ++-- .../system/src/Tests/Database/SchemaTest.php | 2 +- .../database_test/database_test.install | 10 ++-- .../src/Entity/EntityTestStringId.php | 5 +- core/modules/user/src/UserStorageSchema.php | 3 ++ core/modules/user/user.module | 2 + core/modules/views/src/Tests/ViewTestData.php | 2 +- core/scripts/dump-database-d6.sh | 5 ++ .../Tests/Core/Routing/RoutingFixtures.php | 2 +- sites/default/default.settings.php | 4 +- 22 files changed, 108 insertions(+), 84 deletions(-) delete mode 100644 core/modules/block_content/src/BlockContentStorageSchema.php create mode 100644 core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php index e092a982a28d..a94c0d30a9b8 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php @@ -64,7 +64,7 @@ public static function open(array &$connection_options = array()) { // Character set is added to dsn to ensure PDO uses the proper character // set when escaping. This has security implications. See // https://www.drupal.org/node/1201452 for further discussion. - $dsn .= ';charset=utf8'; + $dsn .= ';charset=utf8mb4'; if (!empty($connection_options['database'])) { $dsn .= ';dbname=' . $connection_options['database']; } @@ -92,13 +92,13 @@ public static function open(array &$connection_options = array()) { $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']); // Force MySQL to use the UTF-8 character set. Also set the collation, if a - // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci' - // for UTF-8. + // certain one has been set; otherwise, MySQL defaults to + // 'utf8mb4_general_ci' for utf8mb4. if (!empty($connection_options['collation'])) { - $pdo->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']); + $pdo->exec('SET NAMES utf8mb4 COLLATE ' . $connection_options['collation']); } else { - $pdo->exec('SET NAMES utf8'); + $pdo->exec('SET NAMES utf8mb4'); } // Set MySQL init_commands if not already defined. Default Drupal's MySQL diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php index 8237c79112c8..6326bcf38316 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php @@ -87,7 +87,7 @@ protected function createTableSql($name, $table) { // Provide defaults if needed. $table += array( 'mysql_engine' => 'InnoDB', - 'mysql_character_set' => 'utf8', + 'mysql_character_set' => 'utf8mb4', ); $sql = "CREATE TABLE {" . $name . "} (\n"; @@ -108,8 +108,8 @@ protected function createTableSql($name, $table) { $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set']; // By default, MySQL uses the default collation for new tables, which is - // 'utf8_general_ci' for utf8. If an alternate collation has been set, it - // needs to be explicitly specified. + // 'utf8mb4_general_ci' for utf8mb4. If an alternate collation has been + // set, it needs to be explicitly specified. // @see DatabaseConnection_mysql if (!empty($info['collation'])) { $sql .= ' COLLATE ' . $info['collation']; diff --git a/core/modules/aggregator/src/FeedStorageSchema.php b/core/modules/aggregator/src/FeedStorageSchema.php index d251ed5f1ea0..4d51bdb1e7e2 100644 --- a/core/modules/aggregator/src/FeedStorageSchema.php +++ b/core/modules/aggregator/src/FeedStorageSchema.php @@ -33,7 +33,7 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st break; case 'title': - $this->addSharedTableFieldUniqueKey($storage_definition, $schema); + $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE); break; } } diff --git a/core/modules/block_content/src/BlockContentStorageSchema.php b/core/modules/block_content/src/BlockContentStorageSchema.php deleted file mode 100644 index c1674aef125d..000000000000 --- a/core/modules/block_content/src/BlockContentStorageSchema.php +++ /dev/null @@ -1,52 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\block_content\BlockContentStorageSchema. - */ - -namespace Drupal\block_content; - -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema; -use Drupal\Core\Field\FieldStorageDefinitionInterface; - -/** - * Defines the block content schema handler. - */ -class BlockContentStorageSchema extends SqlContentEntityStorageSchema { - - /** - * {@inheritdoc} - */ - protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { - $schema = parent::getEntitySchema($entity_type, $reset); - - $schema['block_content_field_data']['unique keys'] += array( - 'block_content__info' => array('info', 'langcode'), - ); - - return $schema; - } - - /** - * {@inheritdoc} - */ - protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) { - $schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping); - $field_name = $storage_definition->getName(); - - if ($table_name == 'block_content_field_data') { - switch ($field_name) { - case 'info': - // Improves the performance of the block_content__info index defined - // in getEntitySchema(). - $schema['fields'][$field_name]['not null'] = TRUE; - break; - } - } - - return $schema; - } - -} diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index 82e16f52da1e..cf5e21af546f 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -23,7 +23,6 @@ * bundle_label = @Translation("Custom block type"), * handlers = { * "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage", - * "storage_schema" = "Drupal\block_content\BlockContentStorageSchema", * "access" = "Drupal\block_content\BlockContentAccessControlHandler", * "list_builder" = "Drupal\block_content\BlockContentListBuilder", * "view_builder" = "Drupal\block_content\BlockContentViewBuilder", diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index 8a5bd427011c..511c5d594036 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -252,7 +252,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('URI')) ->setDescription(t('The URI to access the file (either local or remote).')) ->setSetting('max_length', 255) - ->setSetting('case_sensitive', TRUE); + ->setSetting('case_sensitive', TRUE) + ->addConstraint('FileUriUnique'); $fields['filemime'] = BaseFieldDefinition::create('string') ->setLabel(t('File MIME type')) diff --git a/core/modules/file/src/FileStorageSchema.php b/core/modules/file/src/FileStorageSchema.php index f253020bc5c6..e0856f4311f4 100644 --- a/core/modules/file/src/FileStorageSchema.php +++ b/core/modules/file/src/FileStorageSchema.php @@ -30,7 +30,7 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st break; case 'uri': - $this->addSharedTableFieldUniqueKey($storage_definition, $schema, TRUE); + $this->addSharedTableFieldIndex($storage_definition, $schema, TRUE); break; } } diff --git a/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php b/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php new file mode 100644 index 000000000000..1d795056df6a --- /dev/null +++ b/core/modules/file/src/Plugin/Validation/Constraint/FileUriUnique.php @@ -0,0 +1,31 @@ +<?php + +/** + * @file + * Contains \Drupal\file\Plugin\Validation\Constraint\FileUriUnique. + */ + +namespace Drupal\file\Plugin\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; + +/** + * Supports validating file URIs. + * + * @Plugin( + * id = "FileUriUnique", + * label = @Translation("File URI", context = "Validation") + * ) + */ +class FileUriUnique extends Constraint { + + public $message = 'The file %value already exists. Enter a unique file URI.'; + + /** + * {@inheritdoc} + */ + public function validatedBy() { + return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator'; + } + +} diff --git a/core/modules/file/src/Tests/SaveTest.php b/core/modules/file/src/Tests/SaveTest.php index 666b08de91bb..7b4bb180c31b 100644 --- a/core/modules/file/src/Tests/SaveTest.php +++ b/core/modules/file/src/Tests/SaveTest.php @@ -8,6 +8,9 @@ namespace Drupal\file\Tests; use Drupal\file\Entity\File; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; /** * File saving tests. @@ -57,16 +60,27 @@ function testFileSave() { // Try to insert a second file with the same name apart from case insensitivity // to ensure the 'uri' index allows for filenames with different cases. - $uppercase_file = File::create(array( + $uppercase_values = array( 'uid' => 1, 'filename' => 'DRUPLICON.txt', 'uri' => 'public://DRUPLICON.txt', 'filemime' => 'text/plain', 'status' => FILE_STATUS_PERMANENT, - )); + ); + $uppercase_file = File::create($uppercase_values); file_put_contents($uppercase_file->getFileUri(), 'hello world'); + $violations = $uppercase_file->validate(); + $this->assertEqual(count($violations), 0, 'No violations when adding an URI with an existing filename in upper case.'); $uppercase_file->save(); + // Ensure the database URI uniqueness constraint is triggered. + $uppercase_file_duplicate = File::create($uppercase_values); + file_put_contents($uppercase_file_duplicate->getFileUri(), 'hello world'); + $violations = $uppercase_file_duplicate->validate(); + $this->assertEqual(count($violations), 1); + $this->assertEqual($violations[0]->getMessage(), t('The file %value already exists. Enter a unique file URI.', [ + '%value' => $uppercase_file_duplicate->getFileUri(), + ])); // Ensure that file URI entity queries are case sensitive. $fids = \Drupal::entityQuery('file') ->condition('uri', $uppercase_file->getFileUri()) diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php index 42488c7d534f..f1507a3b87e5 100644 --- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php @@ -261,6 +261,12 @@ protected function ensureTables() { foreach ($this->migration->getSourcePlugin()->getIds() as $id_definition) { $mapkey = 'sourceid' . $count++; $source_id_schema[$mapkey] = $this->getFieldSchema($id_definition); + + // With InnoDB, utf8mb4-based primary keys can't be over 191 characters. + // Use ASCII-based primary keys instead. + if (isset($source_id_schema[$mapkey]['type']) && $source_id_schema[$mapkey]['type'] == 'varchar') { + $source_id_schema[$mapkey]['type'] = 'varchar_ascii'; + } $pks[] = $mapkey; } diff --git a/core/modules/migrate_drupal/src/Tests/Table/d6/System.php b/core/modules/migrate_drupal/src/Tests/Table/d6/System.php index 23bcbd66847c..5bf482f9f78a 100644 --- a/core/modules/migrate_drupal/src/Tests/Table/d6/System.php +++ b/core/modules/migrate_drupal/src/Tests/Table/d6/System.php @@ -26,7 +26,7 @@ public function load() { ), 'fields' => array( 'filename' => array( - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'not null' => TRUE, 'length' => '255', 'default' => '', @@ -916,4 +916,4 @@ public function load() { } } -#8867fc0eccc6c8439bff0a269ec597ae +#e15f00f5d9b1c571ee015c40f8fc7b00 diff --git a/core/modules/node/src/Tests/NodeViewTest.php b/core/modules/node/src/Tests/NodeViewTest.php index 2281dd8b816c..c60d44a0a950 100644 --- a/core/modules/node/src/Tests/NodeViewTest.php +++ b/core/modules/node/src/Tests/NodeViewTest.php @@ -33,4 +33,16 @@ public function testHtmlHeadLinks() { $this->assertEqual($result[0]['href'], $node->url()); } + /** + * Tests that we store and retrieve multi-byte UTF-8 characters correctly. + */ + public function testMultiByteUtf8() { + $title = 'ðŸ'; + $this->assertTrue(mb_strlen($title, 'utf-8') < strlen($title), 'Title has multi-byte characters.'); + $node = $this->drupalCreateNode(array('title' => $title)); + $this->drupalGet($node->urlInfo()); + $result = $this->xpath('//span[contains(@class, "field-name-title")]'); + $this->assertEqual((string) $result[0], $title, 'The passed title was returned.'); + } + } diff --git a/core/modules/system/src/Tests/Database/RegressionTest.php b/core/modules/system/src/Tests/Database/RegressionTest.php index 81f962d0da1e..6f87d2a6444b 100644 --- a/core/modules/system/src/Tests/Database/RegressionTest.php +++ b/core/modules/system/src/Tests/Database/RegressionTest.php @@ -26,16 +26,16 @@ class RegressionTest extends DatabaseTestBase { */ function testRegression_310447() { // That's a 255 character UTF-8 string. - $name = str_repeat("é", 255); + $job = str_repeat("é", 255); db_insert('test') ->fields(array( - 'name' => $name, + 'name' => $this->randomMachineName(), 'age' => 20, - 'job' => 'Dancer', + 'job' => $job, ))->execute(); - $from_database = db_query('SELECT name FROM {test} WHERE name = :name', array(':name' => $name))->fetchField(); - $this->assertIdentical($name, $from_database, 'The database handles UTF-8 characters cleanly.'); + $from_database = db_query('SELECT job FROM {test} WHERE job = :job', array(':job' => $job))->fetchField(); + $this->assertIdentical($job, $from_database, 'The database handles UTF-8 characters cleanly.'); } /** diff --git a/core/modules/system/src/Tests/Database/SchemaTest.php b/core/modules/system/src/Tests/Database/SchemaTest.php index 9a234edd5b2d..df1b0b6530c1 100644 --- a/core/modules/system/src/Tests/Database/SchemaTest.php +++ b/core/modules/system/src/Tests/Database/SchemaTest.php @@ -72,7 +72,7 @@ function testSchema() { $columns = db_query('SHOW FULL COLUMNS FROM {test_table}'); foreach ($columns as $column) { if ($column->Field == 'test_field_string') { - $string_check = ($column->Collation == 'utf8_general_ci'); + $string_check = ($column->Collation == 'utf8mb4_general_ci'); } if ($column->Field == 'test_field_string_ascii') { $string_ascii_check = ($column->Collation == 'ascii_general_ci'); diff --git a/core/modules/system/tests/modules/database_test/database_test.install b/core/modules/system/tests/modules/database_test/database_test.install index 7c74c1c4be64..cfd72d5fe357 100644 --- a/core/modules/system/tests/modules/database_test/database_test.install +++ b/core/modules/system/tests/modules/database_test/database_test.install @@ -24,7 +24,7 @@ function database_test_schema() { ), 'name' => array( 'description' => "A person's name", - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, 'default' => '', @@ -75,7 +75,7 @@ function database_test_schema() { ), 'job' => array( 'description' => "The person's job", - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, 'default' => '', @@ -106,7 +106,7 @@ function database_test_schema() { ), 'job' => array( 'description' => "The person's job", - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, 'default' => '', @@ -197,7 +197,7 @@ function database_test_schema() { ), 'name' => array( 'description' => "A person's name.", - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => FALSE, 'default' => '', @@ -228,7 +228,7 @@ function database_test_schema() { ), 'name' => array( 'description' => "A person's name.", - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => FALSE, 'default' => '', diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php index 7e057d4b4aeb..d6b6c59ea129 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php @@ -45,7 +45,10 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['id'] = BaseFieldDefinition::create('string') ->setLabel(t('ID')) ->setDescription(t('The ID of the test entity.')) - ->setReadOnly(TRUE); + ->setReadOnly(TRUE) + // In order to work around the InnoDB 191 character limit on utf8mb4 + // primary keys, we set the character set for the field to ASCII. + ->setSetting('is_ascii', TRUE); return $fields; } diff --git a/core/modules/user/src/UserStorageSchema.php b/core/modules/user/src/UserStorageSchema.php index 447469d34e94..6247b10db157 100644 --- a/core/modules/user/src/UserStorageSchema.php +++ b/core/modules/user/src/UserStorageSchema.php @@ -52,6 +52,9 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st // Improves the performance of the user__name index defined // in getEntitySchema(). $schema['fields'][$field_name]['not null'] = TRUE; + // Make sure the field is no longer than 191 characters so we can + // add a unique constraint in MySQL. + $schema['fields'][$field_name]['length'] = USERNAME_MAX_LENGTH; break; case 'mail': diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 1e6de88c236d..7a3e84329c7c 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -26,6 +26,8 @@ /** * Maximum length of username text field. + * + * Keep this under 191 characters so we can use a unique constraint in MySQL. */ const USERNAME_MAX_LENGTH = 60; diff --git a/core/modules/views/src/Tests/ViewTestData.php b/core/modules/views/src/Tests/ViewTestData.php index 4b5340ea9603..34fcd444dde1 100644 --- a/core/modules/views/src/Tests/ViewTestData.php +++ b/core/modules/views/src/Tests/ViewTestData.php @@ -75,7 +75,7 @@ public static function schemaDefinition() { ), 'name' => array( 'description' => "A person's name", - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, 'default' => '', diff --git a/core/scripts/dump-database-d6.sh b/core/scripts/dump-database-d6.sh index ace93d9d34f6..d0a89d7b1ce8 100644 --- a/core/scripts/dump-database-d6.sh +++ b/core/scripts/dump-database-d6.sh @@ -53,6 +53,11 @@ $schema = drupal_get_schema(); ksort($schema); +// Override the field type of the filename primary key to bypass the +// InnoDB 191 character limitation. +if (isset($schema['system']['primary key']) && $schema['system']['primary key'] == 'filename' && isset($schema['system']['fields']['filename']['type']) && $schema['system']['fields']['filename']['type'] == 'varchar') { + $schema['system']['fields']['filename']['type'] = 'varchar_ascii'; +} // Export all the tables in the schema. foreach ($schema as $table => $data) { // Remove descriptions to save time and code. diff --git a/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php b/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php index 54cd70de7663..05762353d33f 100644 --- a/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php +++ b/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php @@ -172,7 +172,7 @@ public function routingTableDefinition() { 'fields' => array( 'name' => array( 'description' => 'Primary Key: Machine name of this route', - 'type' => 'varchar', + 'type' => 'varchar_ascii', 'length' => 255, 'not null' => TRUE, 'default' => '', diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index ef839391355b..d894b11729f2 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -75,7 +75,7 @@ * 'host' => 'localhost', * 'port' => 3306, * 'prefix' => 'myprefix_', - * 'collation' => 'utf8_general_ci', + * 'collation' => 'utf8mb4_general_ci', * ); * @endcode * @@ -127,7 +127,7 @@ * 'password' => 'password', * 'host' => 'localhost', * 'prefix' => 'main_', - * 'collation' => 'utf8_general_ci', + * 'collation' => 'utf8mb4_general_ci', * ); * @endcode * -- GitLab