Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Database / Driver / mysql / Schema.php
1 <?php
2
3 namespace Drupal\Core\Database\Driver\mysql;
4
5 use Drupal\Core\Database\Query\Condition;
6 use Drupal\Core\Database\SchemaException;
7 use Drupal\Core\Database\SchemaObjectExistsException;
8 use Drupal\Core\Database\SchemaObjectDoesNotExistException;
9 use Drupal\Core\Database\Schema as DatabaseSchema;
10 use Drupal\Component\Utility\Unicode;
11
12 /**
13  * @addtogroup schemaapi
14  * @{
15  */
16
17 /**
18  * MySQL implementation of \Drupal\Core\Database\Schema.
19  */
20 class Schema extends DatabaseSchema {
21
22   /**
23    * Maximum length of a table comment in MySQL.
24    */
25   const COMMENT_MAX_TABLE = 60;
26
27   /**
28    * Maximum length of a column comment in MySQL.
29    */
30   const COMMENT_MAX_COLUMN = 255;
31
32   /**
33    * @var array
34    *   List of MySQL string types.
35    */
36   protected $mysqlStringTypes = [
37     'VARCHAR',
38     'CHAR',
39     'TINYTEXT',
40     'MEDIUMTEXT',
41     'LONGTEXT',
42     'TEXT',
43   ];
44
45   /**
46    * Get information about the table and database name from the prefix.
47    *
48    * @return
49    *   A keyed array with information about the database, table name and prefix.
50    */
51   protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
52     $info = ['prefix' => $this->connection->tablePrefix($table)];
53     if ($add_prefix) {
54       $table = $info['prefix'] . $table;
55     }
56     if (($pos = strpos($table, '.')) !== FALSE) {
57       $info['database'] = substr($table, 0, $pos);
58       $info['table'] = substr($table, ++$pos);
59     }
60     else {
61       $info['database'] = $this->connection->getConnectionOptions()['database'];
62       $info['table'] = $table;
63     }
64     return $info;
65   }
66
67   /**
68    * Build a condition to match a table name against a standard information_schema.
69    *
70    * MySQL uses databases like schemas rather than catalogs so when we build
71    * a condition to query the information_schema.tables, we set the default
72    * database as the schema unless specified otherwise, and exclude table_catalog
73    * from the condition criteria.
74    */
75   protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
76     $table_info = $this->getPrefixInfo($table_name, $add_prefix);
77
78     $condition = new Condition('AND');
79     $condition->condition('table_schema', $table_info['database']);
80     $condition->condition('table_name', $table_info['table'], $operator);
81     return $condition;
82   }
83
84   /**
85    * Generate SQL to create a new table from a Drupal schema definition.
86    *
87    * @param $name
88    *   The name of the table to create.
89    * @param $table
90    *   A Schema API table definition array.
91    * @return
92    *   An array of SQL statements to create the table.
93    */
94   protected function createTableSql($name, $table) {
95     $info = $this->connection->getConnectionOptions();
96
97     // Provide defaults if needed.
98     $table += [
99       'mysql_engine' => 'InnoDB',
100       'mysql_character_set' => 'utf8mb4',
101     ];
102
103     $sql = "CREATE TABLE {" . $name . "} (\n";
104
105     // Add the SQL statement for each field.
106     foreach ($table['fields'] as $field_name => $field) {
107       $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
108     }
109
110     // Process keys & indexes.
111     $keys = $this->createKeysSql($table);
112     if (count($keys)) {
113       $sql .= implode(", \n", $keys) . ", \n";
114     }
115
116     // Remove the last comma and space.
117     $sql = substr($sql, 0, -3) . "\n) ";
118
119     $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
120     // By default, MySQL uses the default collation for new tables, which is
121     // 'utf8mb4_general_ci' for utf8mb4. If an alternate collation has been
122     // set, it needs to be explicitly specified.
123     // @see \Drupal\Core\Database\Driver\mysql\Schema
124     if (!empty($info['collation'])) {
125       $sql .= ' COLLATE ' . $info['collation'];
126     }
127
128     // Add table comment.
129     if (!empty($table['description'])) {
130       $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
131     }
132
133     return [$sql];
134   }
135
136   /**
137    * Create an SQL string for a field to be used in table creation or alteration.
138    *
139    * Before passing a field out of a schema definition into this function it has
140    * to be processed by _db_process_field().
141    *
142    * @param string $name
143    *   Name of the field.
144    * @param array $spec
145    *   The field specification, as per the schema data structure format.
146    */
147   protected function createFieldSql($name, $spec) {
148     $sql = "`" . $name . "` " . $spec['mysql_type'];
149
150     if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) {
151       if (isset($spec['length'])) {
152         $sql .= '(' . $spec['length'] . ')';
153       }
154       if (!empty($spec['binary'])) {
155         $sql .= ' BINARY';
156       }
157       // Note we check for the "type" key here. "mysql_type" is VARCHAR:
158       if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
159         $sql .= ' CHARACTER SET ascii COLLATE ascii_general_ci';
160       }
161     }
162     elseif (isset($spec['precision']) && isset($spec['scale'])) {
163       $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
164     }
165
166     if (!empty($spec['unsigned'])) {
167       $sql .= ' unsigned';
168     }
169
170     if (isset($spec['not null'])) {
171       if ($spec['not null']) {
172         $sql .= ' NOT NULL';
173       }
174       else {
175         $sql .= ' NULL';
176       }
177     }
178
179     if (!empty($spec['auto_increment'])) {
180       $sql .= ' auto_increment';
181     }
182
183     // $spec['default'] can be NULL, so we explicitly check for the key here.
184     if (array_key_exists('default', $spec)) {
185       $sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']);
186     }
187
188     if (empty($spec['not null']) && !isset($spec['default'])) {
189       $sql .= ' DEFAULT NULL';
190     }
191
192     // Add column comment.
193     if (!empty($spec['description'])) {
194       $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
195     }
196
197     return $sql;
198   }
199
200   /**
201    * Set database-engine specific properties for a field.
202    *
203    * @param $field
204    *   A field description array, as specified in the schema documentation.
205    */
206   protected function processField($field) {
207
208     if (!isset($field['size'])) {
209       $field['size'] = 'normal';
210     }
211
212     // Set the correct database-engine specific datatype.
213     // In case one is already provided, force it to uppercase.
214     if (isset($field['mysql_type'])) {
215       $field['mysql_type'] = Unicode::strtoupper($field['mysql_type']);
216     }
217     else {
218       $map = $this->getFieldTypeMap();
219       $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
220     }
221
222     if (isset($field['type']) && $field['type'] == 'serial') {
223       $field['auto_increment'] = TRUE;
224     }
225
226     return $field;
227   }
228
229   /**
230    * {@inheritdoc}
231    */
232   public function getFieldTypeMap() {
233     // Put :normal last so it gets preserved by array_flip. This makes
234     // it much easier for modules (such as schema.module) to map
235     // database types back into schema types.
236     // $map does not use drupal_static as its value never changes.
237     static $map = [
238       'varchar_ascii:normal' => 'VARCHAR',
239
240       'varchar:normal'  => 'VARCHAR',
241       'char:normal'     => 'CHAR',
242
243       'text:tiny'       => 'TINYTEXT',
244       'text:small'      => 'TINYTEXT',
245       'text:medium'     => 'MEDIUMTEXT',
246       'text:big'        => 'LONGTEXT',
247       'text:normal'     => 'TEXT',
248
249       'serial:tiny'     => 'TINYINT',
250       'serial:small'    => 'SMALLINT',
251       'serial:medium'   => 'MEDIUMINT',
252       'serial:big'      => 'BIGINT',
253       'serial:normal'   => 'INT',
254
255       'int:tiny'        => 'TINYINT',
256       'int:small'       => 'SMALLINT',
257       'int:medium'      => 'MEDIUMINT',
258       'int:big'         => 'BIGINT',
259       'int:normal'      => 'INT',
260
261       'float:tiny'      => 'FLOAT',
262       'float:small'     => 'FLOAT',
263       'float:medium'    => 'FLOAT',
264       'float:big'       => 'DOUBLE',
265       'float:normal'    => 'FLOAT',
266
267       'numeric:normal'  => 'DECIMAL',
268
269       'blob:big'        => 'LONGBLOB',
270       'blob:normal'     => 'BLOB',
271     ];
272     return $map;
273   }
274
275   protected function createKeysSql($spec) {
276     $keys = [];
277
278     if (!empty($spec['primary key'])) {
279       $keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')';
280     }
281     if (!empty($spec['unique keys'])) {
282       foreach ($spec['unique keys'] as $key => $fields) {
283         $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')';
284       }
285     }
286     if (!empty($spec['indexes'])) {
287       $indexes = $this->getNormalizedIndexes($spec);
288       foreach ($indexes as $index => $fields) {
289         $keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')';
290       }
291     }
292
293     return $keys;
294   }
295
296   /**
297    * Gets normalized indexes from a table specification.
298    *
299    * Shortens indexes to 191 characters if they apply to utf8mb4-encoded
300    * fields, in order to comply with the InnoDB index limitation of 756 bytes.
301    *
302    * @param array $spec
303    *   The table specification.
304    *
305    * @return array
306    *   List of shortened indexes.
307    *
308    * @throws \Drupal\Core\Database\SchemaException
309    *   Thrown if field specification is missing.
310    */
311   protected function getNormalizedIndexes(array $spec) {
312     $indexes = isset($spec['indexes']) ? $spec['indexes'] : [];
313     foreach ($indexes as $index_name => $index_fields) {
314       foreach ($index_fields as $index_key => $index_field) {
315         // Get the name of the field from the index specification.
316         $field_name = is_array($index_field) ? $index_field[0] : $index_field;
317         // Check whether the field is defined in the table specification.
318         if (isset($spec['fields'][$field_name])) {
319           // Get the MySQL type from the processed field.
320           $mysql_field = $this->processField($spec['fields'][$field_name]);
321           if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) {
322             // Check whether we need to shorten the index.
323             if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) {
324               // Limit the index length to 191 characters.
325               $this->shortenIndex($indexes[$index_name][$index_key]);
326             }
327           }
328         }
329         else {
330           throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
331         }
332       }
333     }
334     return $indexes;
335   }
336
337   /**
338    * Helper function for normalizeIndexes().
339    *
340    * Shortens an index to 191 characters.
341    *
342    * @param array $index
343    *   The index array to be used in createKeySql.
344    *
345    * @see Drupal\Core\Database\Driver\mysql\Schema::createKeySql()
346    * @see Drupal\Core\Database\Driver\mysql\Schema::normalizeIndexes()
347    */
348   protected function shortenIndex(&$index) {
349     if (is_array($index)) {
350       if ($index[1] > 191) {
351         $index[1] = 191;
352       }
353     }
354     else {
355       $index = [$index, 191];
356     }
357   }
358
359   protected function createKeySql($fields) {
360     $return = [];
361     foreach ($fields as $field) {
362       if (is_array($field)) {
363         $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
364       }
365       else {
366         $return[] = '`' . $field . '`';
367       }
368     }
369     return implode(', ', $return);
370   }
371
372   /**
373    * {@inheritdoc}
374    */
375   public function renameTable($table, $new_name) {
376     if (!$this->tableExists($table)) {
377       throw new SchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", ['@table' => $table, '@table_new' => $new_name]));
378     }
379     if ($this->tableExists($new_name)) {
380       throw new SchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", ['@table' => $table, '@table_new' => $new_name]));
381     }
382
383     $info = $this->getPrefixInfo($new_name);
384     return $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
385   }
386
387   /**
388    * {@inheritdoc}
389    */
390   public function dropTable($table) {
391     if (!$this->tableExists($table)) {
392       return FALSE;
393     }
394
395     $this->connection->query('DROP TABLE {' . $table . '}');
396     return TRUE;
397   }
398
399   /**
400    * {@inheritdoc}
401    */
402   public function addField($table, $field, $spec, $keys_new = []) {
403     if (!$this->tableExists($table)) {
404       throw new SchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", ['@field' => $field, '@table' => $table]));
405     }
406     if ($this->fieldExists($table, $field)) {
407       throw new SchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", ['@field' => $field, '@table' => $table]));
408     }
409
410     // Fields that are part of a PRIMARY KEY must be added as NOT NULL.
411     $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE);
412
413     $fixnull = FALSE;
414     if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
415       $fixnull = TRUE;
416       $spec['not null'] = FALSE;
417     }
418     $query = 'ALTER TABLE {' . $table . '} ADD ';
419     $query .= $this->createFieldSql($field, $this->processField($spec));
420     if ($keys_sql = $this->createKeysSql($keys_new)) {
421       // Make sure to drop the existing primary key before adding a new one.
422       // This is only needed when adding a field because this method, unlike
423       // changeField(), is supposed to handle primary keys automatically.
424       if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY')) {
425         $query .= ', DROP PRIMARY KEY';
426       }
427
428       $query .= ', ADD ' . implode(', ADD ', $keys_sql);
429     }
430     $this->connection->query($query);
431     if (isset($spec['initial'])) {
432       $this->connection->update($table)
433         ->fields([$field => $spec['initial']])
434         ->execute();
435     }
436     if (isset($spec['initial_from_field'])) {
437       $this->connection->update($table)
438         ->expression($field, $spec['initial_from_field'])
439         ->execute();
440     }
441     if ($fixnull) {
442       $spec['not null'] = TRUE;
443       $this->changeField($table, $field, $field, $spec);
444     }
445   }
446
447   /**
448    * {@inheritdoc}
449    */
450   public function dropField($table, $field) {
451     if (!$this->fieldExists($table, $field)) {
452       return FALSE;
453     }
454
455     $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
456     return TRUE;
457   }
458
459   /**
460    * {@inheritdoc}
461    */
462   public function fieldSetDefault($table, $field, $default) {
463     if (!$this->fieldExists($table, $field)) {
464       throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", ['@table' => $table, '@field' => $field]));
465     }
466
467     $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $this->escapeDefaultValue($default));
468   }
469
470   /**
471    * {@inheritdoc}
472    */
473   public function fieldSetNoDefault($table, $field) {
474     if (!$this->fieldExists($table, $field)) {
475       throw new SchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", ['@table' => $table, '@field' => $field]));
476     }
477
478     $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
479   }
480
481   /**
482    * {@inheritdoc}
483    */
484   public function indexExists($table, $name) {
485     // Returns one row for each column in the index. Result is string or FALSE.
486     // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
487     $row = $this->connection->query('SHOW INDEX FROM {' . $table . '} WHERE key_name = ' . $this->connection->quote($name))->fetchAssoc();
488     return isset($row['Key_name']);
489   }
490
491   /**
492    * {@inheritdoc}
493    */
494   public function addPrimaryKey($table, $fields) {
495     if (!$this->tableExists($table)) {
496       throw new SchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", ['@table' => $table]));
497     }
498     if ($this->indexExists($table, 'PRIMARY')) {
499       throw new SchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", ['@table' => $table]));
500     }
501
502     $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
503   }
504
505   /**
506    * {@inheritdoc}
507    */
508   public function dropPrimaryKey($table) {
509     if (!$this->indexExists($table, 'PRIMARY')) {
510       return FALSE;
511     }
512
513     $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
514     return TRUE;
515   }
516
517   /**
518    * {@inheritdoc}
519    */
520   public function addUniqueKey($table, $name, $fields) {
521     if (!$this->tableExists($table)) {
522       throw new SchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", ['@table' => $table, '@name' => $name]));
523     }
524     if ($this->indexExists($table, $name)) {
525       throw new SchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", ['@table' => $table, '@name' => $name]));
526     }
527
528     $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
529   }
530
531   /**
532    * {@inheritdoc}
533    */
534   public function dropUniqueKey($table, $name) {
535     if (!$this->indexExists($table, $name)) {
536       return FALSE;
537     }
538
539     $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
540     return TRUE;
541   }
542
543   /**
544    * {@inheritdoc}
545    */
546   public function addIndex($table, $name, $fields, array $spec) {
547     if (!$this->tableExists($table)) {
548       throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", ['@table' => $table, '@name' => $name]));
549     }
550     if ($this->indexExists($table, $name)) {
551       throw new SchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", ['@table' => $table, '@name' => $name]));
552     }
553
554     $spec['indexes'][$name] = $fields;
555     $indexes = $this->getNormalizedIndexes($spec);
556
557     $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
558   }
559
560   /**
561    * {@inheritdoc}
562    */
563   public function dropIndex($table, $name) {
564     if (!$this->indexExists($table, $name)) {
565       return FALSE;
566     }
567
568     $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
569     return TRUE;
570   }
571
572   /**
573    * {@inheritdoc}
574    */
575   public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
576     if (!$this->fieldExists($table, $field)) {
577       throw new SchemaObjectDoesNotExistException(t("Cannot change the definition of field @table.@name: field doesn't exist.", ['@table' => $table, '@name' => $field]));
578     }
579     if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
580       throw new SchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", ['@table' => $table, '@name' => $field, '@name_new' => $field_new]));
581     }
582
583     $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
584     if ($keys_sql = $this->createKeysSql($keys_new)) {
585       $sql .= ', ADD ' . implode(', ADD ', $keys_sql);
586     }
587     $this->connection->query($sql);
588   }
589
590   /**
591    * {@inheritdoc}
592    */
593   public function prepareComment($comment, $length = NULL) {
594     // Truncate comment to maximum comment length.
595     if (isset($length)) {
596       // Add table prefixes before truncating.
597       $comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE);
598     }
599     // Remove semicolons to avoid triggering multi-statement check.
600     $comment = strtr($comment, [';' => '.']);
601     return $this->connection->quote($comment);
602   }
603
604   /**
605    * Retrieve a table or column comment.
606    */
607   public function getComment($table, $column = NULL) {
608     $condition = $this->buildTableNameCondition($table);
609     if (isset($column)) {
610       $condition->condition('column_name', $column);
611       $condition->compile($this->connection, $this);
612       // Don't use {} around information_schema.columns table.
613       return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
614     }
615     $condition->compile($this->connection, $this);
616     // Don't use {} around information_schema.tables table.
617     $comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
618     // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
619     return preg_replace('/; InnoDB free:.*$/', '', $comment);
620   }
621
622   /**
623    * {@inheritdoc}
624    */
625   public function tableExists($table) {
626     // The information_schema table is very slow to query under MySQL 5.0.
627     // Instead, we try to select from the table in question.  If it fails,
628     // the most likely reason is that it does not exist. That is dramatically
629     // faster than using information_schema.
630     // @link http://bugs.mysql.com/bug.php?id=19588
631     // @todo This override should be removed once we require a version of MySQL
632     //   that has that bug fixed.
633     try {
634       $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
635       return TRUE;
636     }
637     catch (\Exception $e) {
638       return FALSE;
639     }
640   }
641
642   /**
643    * {@inheritdoc}
644    */
645   public function fieldExists($table, $column) {
646     // The information_schema table is very slow to query under MySQL 5.0.
647     // Instead, we try to select from the table and field in question. If it
648     // fails, the most likely reason is that it does not exist. That is
649     // dramatically faster than using information_schema.
650     // @link http://bugs.mysql.com/bug.php?id=19588
651     // @todo This override should be removed once we require a version of MySQL
652     //   that has that bug fixed.
653     try {
654       $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
655       return TRUE;
656     }
657     catch (\Exception $e) {
658       return FALSE;
659     }
660   }
661
662 }
663
664 /**
665  * @} End of "addtogroup schemaapi".
666  */