Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / lib / Drupal / Core / Database / Driver / mysql / Connection.php
1 <?php
2
3 namespace Drupal\Core\Database\Driver\mysql;
4
5 use Drupal\Core\Database\DatabaseAccessDeniedException;
6 use Drupal\Core\Database\DatabaseExceptionWrapper;
7
8 use Drupal\Core\Database\Database;
9 use Drupal\Core\Database\DatabaseNotFoundException;
10 use Drupal\Core\Database\TransactionCommitFailedException;
11 use Drupal\Core\Database\DatabaseException;
12 use Drupal\Core\Database\Connection as DatabaseConnection;
13 use Drupal\Component\Utility\Unicode;
14
15 /**
16  * @addtogroup database
17  * @{
18  */
19
20 /**
21  * MySQL implementation of \Drupal\Core\Database\Connection.
22  */
23 class Connection extends DatabaseConnection {
24
25   /**
26    * Error code for "Unknown database" error.
27    */
28   const DATABASE_NOT_FOUND = 1049;
29
30   /**
31    * Error code for "Access denied" error.
32    */
33   const ACCESS_DENIED = 1045;
34
35   /**
36    * Error code for "Can't initialize character set" error.
37    */
38   const UNSUPPORTED_CHARSET = 2019;
39
40   /**
41    * Driver-specific error code for "Unknown character set" error.
42    */
43   const UNKNOWN_CHARSET = 1115;
44
45   /**
46    * SQLSTATE error code for "Syntax error or access rule violation".
47    */
48   const SQLSTATE_SYNTAX_ERROR = 42000;
49
50   /**
51    * Flag to indicate if the cleanup function in __destruct() should run.
52    *
53    * @var bool
54    */
55   protected $needsCleanup = FALSE;
56
57   /**
58    * The minimal possible value for the max_allowed_packet setting of MySQL.
59    *
60    * @link https://mariadb.com/kb/en/mariadb/server-system-variables/#max_allowed_packet
61    * @link https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet
62    *
63    * @var int
64    */
65   const MIN_MAX_ALLOWED_PACKET = 1024;
66
67   /**
68    * The list of MySQL reserved key words.
69    *
70    * @link https://dev.mysql.com/doc/refman/8.0/en/keywords.html
71    */
72   private $reservedKeyWords = [
73     'accessible',
74     'add',
75     'admin',
76     'all',
77     'alter',
78     'analyze',
79     'and',
80     'as',
81     'asc',
82     'asensitive',
83     'before',
84     'between',
85     'bigint',
86     'binary',
87     'blob',
88     'both',
89     'by',
90     'call',
91     'cascade',
92     'case',
93     'change',
94     'char',
95     'character',
96     'check',
97     'collate',
98     'column',
99     'condition',
100     'constraint',
101     'continue',
102     'convert',
103     'create',
104     'cross',
105     'cube',
106     'cume_dist',
107     'current_date',
108     'current_time',
109     'current_timestamp',
110     'current_user',
111     'cursor',
112     'database',
113     'databases',
114     'day_hour',
115     'day_microsecond',
116     'day_minute',
117     'day_second',
118     'dec',
119     'decimal',
120     'declare',
121     'default',
122     'delayed',
123     'delete',
124     'dense_rank',
125     'desc',
126     'describe',
127     'deterministic',
128     'distinct',
129     'distinctrow',
130     'div',
131     'double',
132     'drop',
133     'dual',
134     'each',
135     'else',
136     'elseif',
137     'empty',
138     'enclosed',
139     'escaped',
140     'except',
141     'exists',
142     'exit',
143     'explain',
144     'false',
145     'fetch',
146     'first_value',
147     'float',
148     'float4',
149     'float8',
150     'for',
151     'force',
152     'foreign',
153     'from',
154     'fulltext',
155     'function',
156     'generated',
157     'get',
158     'grant',
159     'group',
160     'grouping',
161     'groups',
162     'having',
163     'high_priority',
164     'hour_microsecond',
165     'hour_minute',
166     'hour_second',
167     'if',
168     'ignore',
169     'in',
170     'index',
171     'infile',
172     'inner',
173     'inout',
174     'insensitive',
175     'insert',
176     'int',
177     'int1',
178     'int2',
179     'int3',
180     'int4',
181     'int8',
182     'integer',
183     'interval',
184     'into',
185     'io_after_gtids',
186     'io_before_gtids',
187     'is',
188     'iterate',
189     'join',
190     'json_table',
191     'key',
192     'keys',
193     'kill',
194     'lag',
195     'last_value',
196     'lead',
197     'leading',
198     'leave',
199     'left',
200     'like',
201     'limit',
202     'linear',
203     'lines',
204     'load',
205     'localtime',
206     'localtimestamp',
207     'lock',
208     'long',
209     'longblob',
210     'longtext',
211     'loop',
212     'low_priority',
213     'master_bind',
214     'master_ssl_verify_server_cert',
215     'match',
216     'maxvalue',
217     'mediumblob',
218     'mediumint',
219     'mediumtext',
220     'middleint',
221     'minute_microsecond',
222     'minute_second',
223     'mod',
224     'modifies',
225     'natural',
226     'not',
227     'no_write_to_binlog',
228     'nth_value',
229     'ntile',
230     'null',
231     'numeric',
232     'of',
233     'on',
234     'optimize',
235     'optimizer_costs',
236     'option',
237     'optionally',
238     'or',
239     'order',
240     'out',
241     'outer',
242     'outfile',
243     'over',
244     'partition',
245     'percent_rank',
246     'persist',
247     'persist_only',
248     'precision',
249     'primary',
250     'procedure',
251     'purge',
252     'range',
253     'rank',
254     'read',
255     'reads',
256     'read_write',
257     'real',
258     'recursive',
259     'references',
260     'regexp',
261     'release',
262     'rename',
263     'repeat',
264     'replace',
265     'require',
266     'resignal',
267     'restrict',
268     'return',
269     'revoke',
270     'right',
271     'rlike',
272     'row',
273     'rows',
274     'row_number',
275     'schema',
276     'schemas',
277     'second_microsecond',
278     'select',
279     'sensitive',
280     'separator',
281     'set',
282     'show',
283     'signal',
284     'smallint',
285     'spatial',
286     'specific',
287     'sql',
288     'sqlexception',
289     'sqlstate',
290     'sqlwarning',
291     'sql_big_result',
292     'sql_calc_found_rows',
293     'sql_small_result',
294     'ssl',
295     'starting',
296     'stored',
297     'straight_join',
298     'system',
299     'table',
300     'terminated',
301     'then',
302     'tinyblob',
303     'tinyint',
304     'tinytext',
305     'to',
306     'trailing',
307     'trigger',
308     'true',
309     'undo',
310     'union',
311     'unique',
312     'unlock',
313     'unsigned',
314     'update',
315     'usage',
316     'use',
317     'using',
318     'utc_date',
319     'utc_time',
320     'utc_timestamp',
321     'values',
322     'varbinary',
323     'varchar',
324     'varcharacter',
325     'varying',
326     'virtual',
327     'when',
328     'where',
329     'while',
330     'window',
331     'with',
332     'write',
333     'xor',
334     'year_month',
335     'zerofill',
336   ];
337
338   /**
339    * Constructs a Connection object.
340    */
341   public function __construct(\PDO $connection, array $connection_options = []) {
342     parent::__construct($connection, $connection_options);
343
344     // This driver defaults to transaction support, except if explicitly passed FALSE.
345     $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
346
347     // MySQL never supports transactional DDL.
348     $this->transactionalDDLSupport = FALSE;
349
350     $this->connectionOptions = $connection_options;
351   }
352
353   /**
354    * {@inheritdoc}
355    */
356   public function query($query, array $args = [], $options = []) {
357     try {
358       return parent::query($query, $args, $options);
359     }
360     catch (DatabaseException $e) {
361       if ($e->getPrevious()->errorInfo[1] == 1153) {
362         // If a max_allowed_packet error occurs the message length is truncated.
363         // This should prevent the error from recurring if the exception is
364         // logged to the database using dblog or the like.
365         $message = Unicode::truncateBytes($e->getMessage(), self::MIN_MAX_ALLOWED_PACKET);
366         $e = new DatabaseExceptionWrapper($message, $e->getCode(), $e->getPrevious());
367       }
368       throw $e;
369     }
370   }
371
372   /**
373    * {@inheritdoc}
374    */
375   public static function open(array &$connection_options = []) {
376     if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) {
377       // Only used during the installer version check, as a fallback from utf8mb4.
378       $charset = 'utf8';
379     }
380     else {
381       $charset = 'utf8mb4';
382     }
383     // The DSN should use either a socket or a host/port.
384     if (isset($connection_options['unix_socket'])) {
385       $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
386     }
387     else {
388       // Default to TCP connection on port 3306.
389       $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
390     }
391     // Character set is added to dsn to ensure PDO uses the proper character
392     // set when escaping. This has security implications. See
393     // https://www.drupal.org/node/1201452 for further discussion.
394     $dsn .= ';charset=' . $charset;
395     if (!empty($connection_options['database'])) {
396       $dsn .= ';dbname=' . $connection_options['database'];
397     }
398     // Allow PDO options to be overridden.
399     $connection_options += [
400       'pdo' => [],
401     ];
402     $connection_options['pdo'] += [
403       \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
404       // So we don't have to mess around with cursors and unbuffered queries by default.
405       \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
406       // Make sure MySQL returns all matched rows on update queries including
407       // rows that actually didn't have to be updated because the values didn't
408       // change. This matches common behavior among other database systems.
409       \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE,
410       // Because MySQL's prepared statements skip the query cache, because it's dumb.
411       \PDO::ATTR_EMULATE_PREPARES => TRUE,
412     ];
413     if (defined('\PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
414       // An added connection option in PHP 5.5.21 to optionally limit SQL to a
415       // single statement like mysqli.
416       $connection_options['pdo'] += [\PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE];
417     }
418
419     try {
420       $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
421     }
422     catch (\PDOException $e) {
423       if ($e->getCode() == static::DATABASE_NOT_FOUND) {
424         throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
425       }
426       if ($e->getCode() == static::ACCESS_DENIED) {
427         throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
428       }
429       throw $e;
430     }
431
432     // Force MySQL to use the UTF-8 character set. Also set the collation, if a
433     // certain one has been set; otherwise, MySQL defaults to
434     // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
435     // utf8mb4.
436     if (!empty($connection_options['collation'])) {
437       $pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
438     }
439     else {
440       $pdo->exec('SET NAMES ' . $charset);
441     }
442
443     // Set MySQL init_commands if not already defined.  Default Drupal's MySQL
444     // behavior to conform more closely to SQL standards.  This allows Drupal
445     // to run almost seamlessly on many different kinds of database systems.
446     // These settings force MySQL to behave the same as postgresql, or sqlite
447     // in regards to syntax interpretation and invalid data handling.  See
448     // https://www.drupal.org/node/344575 for further discussion. Also, as MySQL
449     // 5.5 changed the meaning of TRADITIONAL we need to spell out the modes one
450     // by one.
451     $connection_options += [
452       'init_commands' => [],
453     ];
454
455     $sql_mode = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,ONLY_FULL_GROUP_BY';
456     // NO_AUTO_CREATE_USER is removed in MySQL 8.0.11
457     // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal
458     $version_server = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION);
459     if (version_compare($version_server, '8.0.11', '<')) {
460       $sql_mode .= ',NO_AUTO_CREATE_USER';
461     }
462     $connection_options['init_commands'] += [
463       'sql_mode' => "SET sql_mode = '$sql_mode'",
464     ];
465
466     // Execute initial commands.
467     foreach ($connection_options['init_commands'] as $sql) {
468       $pdo->exec($sql);
469     }
470
471     return $pdo;
472   }
473
474   /**
475    * {@inheritdoc}
476    */
477   public function escapeField($field) {
478     $field = parent::escapeField($field);
479     return $this->quoteIdentifier($field);
480   }
481
482   /**
483    * {@inheritdoc}
484    */
485   public function escapeAlias($field) {
486     // Quote fields so that MySQL reserved words like 'function' can be used
487     // as aliases.
488     $field = parent::escapeAlias($field);
489     return $this->quoteIdentifier($field);
490   }
491
492   /**
493    * Quotes an identifier if it matches a MySQL reserved keyword.
494    *
495    * @param string $identifier
496    *   The field to check.
497    *
498    * @return string
499    *   The identifier, quoted if it matches a MySQL reserved keyword.
500    */
501   private function quoteIdentifier($identifier) {
502     // Quote identifiers so that MySQL reserved words like 'function' can be
503     // used as column names. Sometimes the 'table.column_name' format is passed
504     // in. For example,
505     // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery() adds a
506     // condition on "base.uid" while loading user entities.
507     if (strpos($identifier, '.') !== FALSE) {
508       list($table, $identifier) = explode('.', $identifier, 2);
509     }
510     if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) {
511       // Quote the string for MySQL reserved keywords.
512       $identifier = '"' . $identifier . '"';
513     }
514     return isset($table) ? $table . '.' . $identifier : $identifier;
515   }
516
517   /**
518    * {@inheritdoc}
519    */
520   public function serialize() {
521     // Cleanup the connection, much like __destruct() does it as well.
522     if ($this->needsCleanup) {
523       $this->nextIdDelete();
524     }
525     $this->needsCleanup = FALSE;
526
527     return parent::serialize();
528   }
529
530   /**
531    * {@inheritdoc}
532    */
533   public function __destruct() {
534     if ($this->needsCleanup) {
535       $this->nextIdDelete();
536     }
537   }
538
539   public function queryRange($query, $from, $count, array $args = [], array $options = []) {
540     return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
541   }
542
543   public function queryTemporary($query, array $args = [], array $options = []) {
544     $tablename = $this->generateTemporaryTableName();
545     $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
546     return $tablename;
547   }
548
549   public function driver() {
550     return 'mysql';
551   }
552
553   public function databaseType() {
554     return 'mysql';
555   }
556
557   /**
558    * Overrides \Drupal\Core\Database\Connection::createDatabase().
559    *
560    * @param string $database
561    *   The name of the database to create.
562    *
563    * @throws \Drupal\Core\Database\DatabaseNotFoundException
564    */
565   public function createDatabase($database) {
566     // Escape the database name.
567     $database = Database::getConnection()->escapeDatabase($database);
568
569     try {
570       // Create the database and set it as active.
571       $this->connection->exec("CREATE DATABASE $database");
572       $this->connection->exec("USE $database");
573     }
574     catch (\Exception $e) {
575       throw new DatabaseNotFoundException($e->getMessage());
576     }
577   }
578
579   public function mapConditionOperator($operator) {
580     // We don't want to override any of the defaults.
581     return NULL;
582   }
583
584   public function nextId($existing_id = 0) {
585     $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
586     // This should only happen after an import or similar event.
587     if ($existing_id >= $new_id) {
588       // If we INSERT a value manually into the sequences table, on the next
589       // INSERT, MySQL will generate a larger value. However, there is no way
590       // of knowing whether this value already exists in the table. MySQL
591       // provides an INSERT IGNORE which would work, but that can mask problems
592       // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
593       // UPDATE in such a way that the UPDATE does not do anything. This way,
594       // duplicate keys do not generate errors but everything else does.
595       $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', [':value' => $existing_id]);
596       $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
597     }
598     $this->needsCleanup = TRUE;
599     return $new_id;
600   }
601
602   public function nextIdDelete() {
603     // While we want to clean up the table to keep it up from occupying too
604     // much storage and memory, we must keep the highest value in the table
605     // because InnoDB uses an in-memory auto-increment counter as long as the
606     // server runs. When the server is stopped and restarted, InnoDB
607     // reinitializes the counter for each table for the first INSERT to the
608     // table based solely on values from the table so deleting all values would
609     // be a problem in this case. Also, TRUNCATE resets the auto increment
610     // counter.
611     try {
612       $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
613       // We know we are using MySQL here, no need for the slower db_delete().
614       $this->query('DELETE FROM {sequences} WHERE value < :value', [':value' => $max_id]);
615     }
616     // During testing, this function is called from shutdown with the
617     // simpletest prefix stored in $this->connection, and those tables are gone
618     // by the time shutdown is called so we need to ignore the database
619     // errors. There is no problem with completely ignoring errors here: if
620     // these queries fail, the sequence will work just fine, just use a bit
621     // more database storage and memory.
622     catch (DatabaseException $e) {
623     }
624   }
625
626   /**
627    * Overridden to work around issues to MySQL not supporting transactional DDL.
628    */
629   protected function popCommittableTransactions() {
630     // Commit all the committable layers.
631     foreach (array_reverse($this->transactionLayers) as $name => $active) {
632       // Stop once we found an active transaction.
633       if ($active) {
634         break;
635       }
636
637       // If there are no more layers left then we should commit.
638       unset($this->transactionLayers[$name]);
639       if (empty($this->transactionLayers)) {
640         if (!$this->connection->commit()) {
641           throw new TransactionCommitFailedException();
642         }
643       }
644       else {
645         // Attempt to release this savepoint in the standard way.
646         try {
647           $this->query('RELEASE SAVEPOINT ' . $name);
648         }
649         catch (DatabaseExceptionWrapper $e) {
650           // However, in MySQL (InnoDB), savepoints are automatically committed
651           // when tables are altered or created (DDL transactions are not
652           // supported). This can cause exceptions due to trying to release
653           // savepoints which no longer exist.
654           //
655           // To avoid exceptions when no actual error has occurred, we silently
656           // succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
657           if ($e->getPrevious()->errorInfo[1] == '1305') {
658             // If one SAVEPOINT was released automatically, then all were.
659             // Therefore, clean the transaction stack.
660             $this->transactionLayers = [];
661             // We also have to explain to PDO that the transaction stack has
662             // been cleaned-up.
663             $this->connection->commit();
664           }
665           else {
666             throw $e;
667           }
668         }
669       }
670     }
671   }
672
673 }
674
675
676 /**
677  * @} End of "addtogroup database".
678  */