sources.db.mysql.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. <?php
  2. /**
  3. * @file
  4. */
  5. require_once dirname(__FILE__) . '/sources.db.inc';
  6. /**
  7. * @file
  8. * Functions to handle the direct to database source.
  9. */
  10. /**
  11. * A source type for backing up from database server.
  12. *
  13. * @ingroup backup_migrate_destinations
  14. */
  15. class backup_migrate_source_db_mysql extends backup_migrate_source_db {
  16. /**
  17. * The table's data keyed by table name.
  18. *
  19. * @var array
  20. */
  21. protected static $tableData = array();
  22. /**
  23. * The tables keyed by name.
  24. *
  25. * @var array
  26. */
  27. protected static $tableNames = array();
  28. /**
  29. * The views keyed by name.
  30. *
  31. * @var array
  32. */
  33. protected static $viewNames = array();
  34. /**
  35. *
  36. */
  37. public function type_name() {
  38. return t("MySQL Database");
  39. }
  40. /**
  41. * Return a list of backup filetypes.
  42. */
  43. public function file_types() {
  44. return array(
  45. "sql" => array(
  46. "extension" => "sql",
  47. "filemime" => "text/x-sql",
  48. "backup" => TRUE,
  49. "restore" => TRUE,
  50. ),
  51. "mysql" => array(
  52. "extension" => "mysql",
  53. "filemime" => "text/x-sql",
  54. "backup" => TRUE,
  55. "restore" => TRUE,
  56. ),
  57. );
  58. }
  59. /**
  60. * Return the scheme for this db type.
  61. */
  62. public function default_scheme() {
  63. return 'mysql';
  64. }
  65. /**
  66. * Declare any mysql databases defined in the settings.php file as a possible source.
  67. */
  68. public function sources() {
  69. $out = array();
  70. global $databases;
  71. foreach ((array) $databases as $db_key => $target) {
  72. foreach ((array) $target as $tgt_key => $info) {
  73. // Only mysql/mysqli supported by this source.
  74. $key = $db_key . ':' . $tgt_key;
  75. if ($info['driver'] === 'mysql') {
  76. // Compile the database connection string.
  77. $url = 'mysql://';
  78. $url .= urlencode($info['username']) . ':' . urlencode($info['password']);
  79. $url .= '@';
  80. $url .= urlencode($info['host']);
  81. if (!empty($info['port'])) {
  82. $url .= ':' . $info['port'];
  83. }
  84. $url .= '/' . urlencode($info['database']);
  85. if ($source = backup_migrate_create_destination('mysql', array('url' => $url))) {
  86. // Treat the default database differently because it is probably
  87. // the only one available.
  88. if ($key == 'default:default') {
  89. $source->set_id('db');
  90. $source->set_name(t('Default Database'));
  91. // Dissalow backing up to the default database because that's confusing and potentially dangerous.
  92. $source->remove_op('scheduled backup');
  93. $source->remove_op('manual backup');
  94. }
  95. else {
  96. $source->set_id('db:' . $key);
  97. $source->set_name($key . ": " . $source->get_display_location());
  98. }
  99. $out[$source->get_id()] = $source;
  100. }
  101. }
  102. }
  103. }
  104. return $out;
  105. }
  106. /**
  107. * Get the file type for to backup this source to.
  108. */
  109. public function get_file_type_id() {
  110. return 'mysql';
  111. }
  112. /**
  113. * Backup the databases to a file.
  114. *
  115. * Returns a list of sql commands, one command per line.
  116. * That makes it easier to import without loading the whole file into memory.
  117. * The files are a little harder to read, but human-readability is not a priority.
  118. */
  119. public function _backup_db_to_file($file, $settings) {
  120. if (!empty($settings->filters['use_cli']) && $this->_backup_db_to_file_mysqldump($file, $settings)) {
  121. return TRUE;
  122. }
  123. $lines = 0;
  124. $exclude = !empty($settings->filters['exclude_tables']) ? $settings->filters['exclude_tables'] : array();
  125. $nodata = !empty($settings->filters['nodata_tables']) ? $settings->filters['nodata_tables'] : array();
  126. if ($file->open(TRUE)) {
  127. $file->write($this->_get_sql_file_header());
  128. $alltables = $this->_get_tables();
  129. $allviews = $this->_get_views();
  130. foreach ($alltables as $table) {
  131. if (_backup_migrate_check_timeout()) {
  132. return FALSE;
  133. }
  134. if ($table['name'] && !isset($exclude[$table['name']])) {
  135. $file->write($this->_get_table_structure_sql($table));
  136. $lines++;
  137. if (!in_array($table['name'], $nodata)) {
  138. $lines += $this->_dump_table_data_sql_to_file($file, $table);
  139. }
  140. }
  141. }
  142. foreach ($allviews as $view) {
  143. if (_backup_migrate_check_timeout()) {
  144. return FALSE;
  145. }
  146. if ($view['name'] && !isset($exclude[$view['name']])) {
  147. $file->write($this->_get_view_create_sql($view));
  148. }
  149. }
  150. $file->write($this->_get_sql_file_footer());
  151. $file->close();
  152. return $lines;
  153. }
  154. else {
  155. return FALSE;
  156. }
  157. }
  158. /**
  159. * Backup the databases to a file using the mysqldump command.
  160. */
  161. public function _backup_db_to_file_mysqldump($file, $settings) {
  162. $success = FALSE;
  163. $nodata_tables = array();
  164. $alltables = $this->_get_tables();
  165. $command = 'mysqldump --result-file=%file --opt -Q --host=%host --port=%port --user=%user --password=%pass %db';
  166. $args = array(
  167. '%file' => $file->filepath(),
  168. '%host' => $this->dest_url['host'],
  169. '%port' => isset($this->dest_url['port']) ? $this->dest_url['port'] : '3306',
  170. '%user' => $this->dest_url['user'],
  171. '%pass' => $this->dest_url['pass'],
  172. '%db' => $this->dest_url['path'],
  173. );
  174. // Ignore the excluded and no-data tables.
  175. $db = $this->dest_url['path'];
  176. if (!empty($settings->filters['exclude_tables'])) {
  177. foreach ((array) $settings->filters['exclude_tables'] as $table) {
  178. if (isset($alltables[$table])) {
  179. $command .= ' --ignore-table=' . $db . '.' . $table;
  180. }
  181. }
  182. }
  183. if (!empty($settings->filters['nodata_tables'])) {
  184. foreach ((array) $settings->filters['nodata_tables'] as $table) {
  185. if (isset($alltables[$table])) {
  186. $nodata_tables[] = $table;
  187. $command .= ' --ignore-table=' . $db . '.' . $table;
  188. }
  189. }
  190. }
  191. $success = backup_migrate_exec($command, $args);
  192. // Get the nodata tables.
  193. if ($success && !empty($nodata_tables)) {
  194. $tables = implode(' ', array_unique($nodata_tables));
  195. $command = "mysqldump --no-data --opt -Q --host=%host --port=%port --user=%user --password=%pass %db $tables >> %file";
  196. $success = backup_migrate_exec($command, $args);
  197. }
  198. return $success;
  199. }
  200. /**
  201. * Backup the databases to a file.
  202. */
  203. public function _restore_db_from_file($file, $settings) {
  204. $num = 0;
  205. if ($file->open() && $conn = $this->_get_db_connection()) {
  206. // Optionally drop all existing tables.
  207. if (!empty($settings->filters['utils_drop_all_tables'])) {
  208. $all_tables = $this->_get_tables();
  209. $table_names = array_map('backup_migrate_array_name_value', $all_tables);
  210. $table_list = join(', ', $table_names);
  211. $stmt = $conn->prepare("DROP TABLE IF EXISTS $table_list;\n");
  212. $stmt->execute();
  213. }
  214. // Read one line at a time and run the query.
  215. while ($line = $this->_read_sql_command_from_file($file)) {
  216. if (_backup_migrate_check_timeout()) {
  217. return FALSE;
  218. }
  219. if ($line) {
  220. // Prepeare and exexute the statement instead of the api function to avoid substitution of '{' etc.
  221. $stmt = $conn->prepare($line);
  222. $stmt->execute();
  223. $num++;
  224. }
  225. }
  226. // Close the file with fclose/gzclose.
  227. $file->close();
  228. }
  229. else {
  230. drupal_set_message(t("Unable to open file %file to restore database", array("%file" => $file->filepath())), 'error');
  231. $num = FALSE;
  232. }
  233. return $num;
  234. }
  235. /**
  236. * Read a multiline sql command from a file.
  237. *
  238. * Supports the formatting created by mysqldump, but won't handle multiline comments.
  239. */
  240. public function _read_sql_command_from_file($file) {
  241. $out = '';
  242. while ($line = $file->read()) {
  243. $first2 = substr($line, 0, 2);
  244. $first3 = substr($line, 0, 3);
  245. // Ignore single line comments. This function doesn't support multiline comments or inline comments.
  246. if ($first2 != '--' && ($first2 != '/*' || $first3 == '/*!')) {
  247. $out .= ' ' . trim($line);
  248. // If a line ends in ; or */ it is a sql command.
  249. if (substr($out, strlen($out) - 1, 1) == ';') {
  250. return trim($out);
  251. }
  252. }
  253. }
  254. return trim($out);
  255. }
  256. /**
  257. * Get a list of tables in the database.
  258. */
  259. public function _get_table_names() {
  260. if (empty(static::$tableNames)) {
  261. static::$tableNames = $this->query("SHOW FULL TABLES WHERE Table_Type = 'BASE TABLE'")
  262. ->fetchAllKeyed(0, 0);
  263. }
  264. return static::$tableNames;
  265. }
  266. /**
  267. * Get a list of views in the database.
  268. */
  269. public function _get_view_names() {
  270. if (empty(static::$viewNames)) {
  271. static::$viewNames = $this->query("SHOW FULL TABLES WHERE Table_Type = 'VIEW'")
  272. ->fetchAllKeyed(0, 0);
  273. }
  274. return static::$viewNames;
  275. }
  276. /**
  277. * Lock the list of given tables in the database.
  278. */
  279. public function _lock_tables($tables) {
  280. if ($tables) {
  281. $tables_escaped = array();
  282. foreach ($tables as $table) {
  283. $tables_escaped[] = '`' . db_escape_table($table) . '` WRITE';
  284. }
  285. $this->query('LOCK TABLES ' . implode(', ', $tables_escaped));
  286. }
  287. }
  288. /**
  289. * Unlock all tables in the database.
  290. */
  291. public function _unlock_tables($settings) {
  292. $this->query('UNLOCK TABLES');
  293. }
  294. /**
  295. * Get a list of table and view data in the db.
  296. */
  297. protected function get_table_data() {
  298. if (empty(static::$tableData)) {
  299. $tables = $this->query('SHOW TABLE STATUS')->fetchAll(PDO::FETCH_ASSOC);
  300. foreach ($tables as $table) {
  301. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the
  302. // default query behavior was changed.
  303. // See: http://drupal.org/node/1171866
  304. $table = array_change_key_case($table);
  305. static::$tableData[$table['name']] = $table;
  306. }
  307. }
  308. return static::$tableData;
  309. }
  310. /**
  311. * Get a list of tables in the db.
  312. */
  313. public function _get_tables() {
  314. $out = array();
  315. foreach ($this->get_table_data() as $table) {
  316. if (!empty($table['engine'])) {
  317. $out[$table['name']] = $table;
  318. }
  319. }
  320. return $out;
  321. }
  322. /**
  323. * Get a list of views in the db.
  324. */
  325. public function _get_views() {
  326. $out = array();
  327. foreach ($this->get_table_data() as $table) {
  328. if (empty($table['engine'])) {
  329. $out[$table['name']] = $table;
  330. }
  331. }
  332. return $out;
  333. }
  334. /**
  335. * Get the sql for the structure of the given view.
  336. */
  337. public function _get_view_create_sql($view) {
  338. $out = "";
  339. // Switch SQL mode to get rid of "CREATE ALGORITHM..." what requires more permissions + troubles with the DEFINER user.
  340. $sql_mode = $this->query("SELECT @@SESSION.sql_mode")->fetchField();
  341. $this->query("SET sql_mode = 'ANSI'");
  342. $result = $this->query("SHOW CREATE VIEW `" . $view['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
  343. $this->query("SET SQL_mode = :mode", array(':mode' => $sql_mode));
  344. foreach ($result as $create) {
  345. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
  346. // See: http://drupal.org/node/1171866
  347. $create = array_change_key_case($create);
  348. $out .= "DROP VIEW IF EXISTS `" . $view['name'] . "`;\n";
  349. $out .= "SET sql_mode = 'ANSI';\n";
  350. $out .= strtr($create['create view'], "\n", " ") . ";\n";
  351. $out .= "SET sql_mode = '$sql_mode';\n";
  352. }
  353. return $out;
  354. }
  355. /**
  356. * Get the sql for the structure of the given table.
  357. */
  358. public function _get_table_structure_sql($table) {
  359. $out = "";
  360. $result = $this->query("SHOW CREATE TABLE `" . $table['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
  361. foreach ($result as $create) {
  362. // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
  363. // See: http://drupal.org/node/1171866
  364. $create = array_change_key_case($create);
  365. $out .= "DROP TABLE IF EXISTS `" . $table['name'] . "`;\n";
  366. // Remove newlines and convert " to ` because PDO seems to convert those for some reason.
  367. $out .= strtr($create['create table'], array("\n" => ' ', '"' => '`'));
  368. if ($table['auto_increment']) {
  369. $out .= " AUTO_INCREMENT=" . $table['auto_increment'];
  370. }
  371. $out .= ";\n";
  372. }
  373. return $out;
  374. }
  375. /**
  376. * Get the sql to insert the data for a given table.
  377. */
  378. public function _dump_table_data_sql_to_file($file, $table) {
  379. $rows_per_query = variable_get('backup_migrate_data_rows_per_query', BACKUP_MIGRATE_DATA_ROWS_PER_QUERY);
  380. $rows_per_line = variable_get('backup_migrate_data_rows_per_line', BACKUP_MIGRATE_DATA_ROWS_PER_LINE);
  381. $bytes_per_line = variable_get('backup_migrate_data_bytes_per_line', BACKUP_MIGRATE_DATA_BYTES_PER_LINE);
  382. if (variable_get('backup_migrate_verbose')) {
  383. _backup_migrate_message('Table: %table', array('%table' => $table['name']), 'success');
  384. }
  385. // Escape backslashes, PHP code, special chars.
  386. $search = array('\\', "'", "\x00", "\x0a", "\x0d", "\x1a");
  387. $replace = array('\\\\', "''", '\0', '\n', '\r', '\Z');
  388. $lines = 0;
  389. $from = 0;
  390. $args = array('fetch' => PDO::FETCH_ASSOC);
  391. while ($data = $this->query("SELECT * FROM `" . $table['name'] . "`", array(), $args, $from, $rows_per_query)) {
  392. if ($data->rowCount() == 0) {
  393. break;
  394. }
  395. $rows = $bytes = 0;
  396. $line = array();
  397. foreach ($data as $row) {
  398. $from++;
  399. // DB Escape the values.
  400. $items = array();
  401. foreach ($row as $key => $value) {
  402. $items[] = is_null($value) ? "null" : "'" . str_replace($search, $replace, $value) . "'";
  403. }
  404. // If there is a row to be added.
  405. if ($items) {
  406. // Start a new line if we need to.
  407. if ($rows == 0) {
  408. $file->write("INSERT INTO `" . $table['name'] . "` VALUES ");
  409. $bytes = $rows = 0;
  410. }
  411. // Otherwise add a comma to end the previous entry.
  412. else {
  413. $file->write(",");
  414. }
  415. // Write the data itself.
  416. $sql = implode(',', $items);
  417. $file->write('(' . $sql . ')');
  418. $bytes += strlen($sql);
  419. $rows++;
  420. // Finish the last line if we've added enough items.
  421. if ($rows >= $rows_per_line || $bytes >= $bytes_per_line) {
  422. $file->write(";\n");
  423. $lines++;
  424. $bytes = $rows = 0;
  425. }
  426. }
  427. }
  428. // Finish any unfinished insert statements.
  429. if ($rows > 0) {
  430. $file->write(";\n");
  431. $lines++;
  432. }
  433. }
  434. if (variable_get('backup_migrate_verbose')) {
  435. _backup_migrate_message('Peak memory usage: %mem', array('%mem' => backup_migrate_get_peak_memory_usage() . 'MB'), 'success');
  436. }
  437. return $lines;
  438. }
  439. /**
  440. * Get the db connection for the specified db.
  441. */
  442. public function _get_db_connection() {
  443. if (!$this->connection) {
  444. $this->connection = parent::_get_db_connection();
  445. // Set the sql mode because the default is ANSI,TRADITIONAL which is not aware of collation or storage engine.
  446. $this->connection->exec("SET sql_mode=''");
  447. }
  448. return $this->connection;
  449. }
  450. /**
  451. * Run a query on this source's database using Drupal's MySQL engine.
  452. *
  453. * @param string $query
  454. * The query string.
  455. * @param array $args
  456. * Arguments for the query.
  457. * @param array $options
  458. * Options to pass to the query.
  459. * @param int|null $from
  460. * The starting point for the query; when passed will perform a queryRange()
  461. * method instead of a regular query().
  462. * @param int|null $count
  463. * The number of records to obtain from this query. Will be ignored if the
  464. * $from argument is empty.
  465. *
  466. * @see DatabaseConnection_mysql::query()
  467. * @see DatabaseConnection_mysql::queryRange()
  468. */
  469. public function query($query, array $args = array(), array $options = array(), $from = NULL, $count = NULL) {
  470. if ($conn = $this->_get_db_connection()) {
  471. // If no $from is passed in, just do a basic query.
  472. if (is_null($from)) {
  473. return $conn->query($query, $args, $options);
  474. }
  475. // The $from variable was passed in, so do a ranged query.
  476. else {
  477. return $conn->queryRange($query, $from, $count, $args, $options);
  478. }
  479. }
  480. }
  481. /**
  482. * The header for the top of the sql dump file. These commands set the connection
  483. * character encoding to help prevent encoding conversion issues.
  484. */
  485. public function _get_sql_file_header() {
  486. $info = $this->_db_info();
  487. return "-- Backup and Migrate (Drupal) MySQL Dump
  488. -- Backup and Migrate Version: " . BACKUP_MIGRATE_VERSION . "
  489. -- http://drupal.org/project/backup_migrate
  490. -- Drupal Version: " . VERSION . "
  491. -- http://drupal.org/
  492. --
  493. -- Host: " . url('', array('absolute' => TRUE)) . "
  494. -- Site Name: " . url('', array('absolute' => TRUE)) . "
  495. -- Generation Time: " . format_date(time(), 'custom', 'r') . "
  496. -- MySQL Version: " . $info['version'] . "
  497. /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
  498. /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
  499. /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
  500. /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
  501. /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
  502. /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */;
  503. SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";
  504. SET NAMES utf8mb4;
  505. ";
  506. }
  507. /**
  508. * The footer of the sql dump file.
  509. */
  510. public function _get_sql_file_footer() {
  511. return "
  512. /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
  513. /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
  514. /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
  515. /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
  516. /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
  517. /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
  518. ";
  519. }
  520. /**
  521. * Get the version info for the given DB.
  522. */
  523. public function _db_info() {
  524. $db = $this->_get_db_connection();
  525. return array(
  526. 'type' => 'mysql',
  527. 'version' => $db ? $db->version() : t('Unknown'),
  528. );
  529. }
  530. }