Connection.php 18 KB

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