#!/usr/bin/env php boot(); $kernel->loadLegacyIncludes(); $options = getopt('', array('database:', 'dump', 'restore', 'validate', 'core:')); if (isset($options['dump'])) { if (empty($options['database'])) { echo "Missing required --database option.\n"; return; } $connection = Database::getConnection('default', $options['database']); $connection_info = $connection->getConnectionOptions(); $version = _get_core_version_from_database($connection); $output_folder = DRUPAL_ROOT . '/core/modules/migrate_drupal/src/Tests/Table/' . $version; $class_template = 'createTable("{{TABLE}}", {{TABLE_DEFINITION}}); $this->database->insert("{{TABLE}}")->fields({{PHP_FIELDS}}) {{PHP_VALUES}}->execute(); } } '; // Generate a list of tables. $tables = $connection->query('SHOW TABLES')->fetchCol(); // Get all character sets, keyed by table name. $character_sets = $connection->query('SELECT T.TABLE_NAME, CCSA.CHARACTER_SET_NAME FROM information_schema.TABLES T INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY CCSA ON CCSA.COLLATION_NAME = T.TABLE_COLLATION WHERE T.TABLE_SCHEMA = \'' . $connection_info['database'] . '\'') ->fetchAllKeyed(); foreach ($tables as $table) { // Generate the class name. $class = Inflector::classify($table); // Order by primary keys $order = ''; $query = "SELECT `COLUMN_NAME` FROM `information_schema`.`COLUMNS` WHERE (`TABLE_SCHEMA` = '" . $connection_info['database'] . "') AND (`TABLE_NAME` = '{$table}') AND (`COLUMN_KEY` = 'PRI') ORDER BY COLUMN_NAME"; $results = $connection->query($query); while(($row = $results->fetchAssoc()) !== FALSE) { $order .= '{' . $row['COLUMN_NAME'] . '}, '; } if (!(empty($order))) { $order = rtrim ($order, ", "); $order = ' ORDER BY ' . $order; } // Generate the field values. $query = $connection->query(_db_get_query($table) . $order); $values = ''; // Only dump the actual table values if we're NOT looking at a cache table, // watchdog or sessions tables. if (substr($table, 0, 5) !== 'cache' && !in_array($table, array('watchdog', 'sessions'))) { while(($row = $query->fetchAssoc()) !== FALSE) { $values .= '->values(' . Variable::export($row, ' ') . ')'; } } // Generate the field names. $query = $connection->query("SHOW COLUMNS FROM {$table}"); $definition = []; while(($row = $query->fetchAssoc()) !== FALSE) { $field_name = $row['Field']; // Parse out the field type and meta information. preg_match('@([a-z]+)(?:\((\d+)(?:,(\d+))?\))?\s*(unsigned)?@', $row['Type'], $matches); $field_type = _db_field_type_map($matches[1]); // If it's auto-increment then make it a serial instead. if ($row['Extra'] === 'auto_increment') { $field_type = 'serial'; } // Add primary key entries as needed. if ($row['Key'] === 'PRI') { $definition['primary key'][] = $field_name; } // All fields have a type and not null. $definition['fields'][$field_name] = [ 'type' => $field_type, 'not null' => $row['Null'] === 'NO', ]; // If this is a numeric field, the meta will be precision and scale. if (isset($matches[2]) && $field_type === 'numeric') { $definition['fields'][$field_name]['precision'] = $matches[2]; $definition['fields'][$field_name]['scale'] = $matches[3]; } elseif ($field_type === 'time' || $field_type === 'datetime') { // We use varchar to replace the D6 datetime and time fields. $definition['fields'][$field_name]['type'] = 'varchar'; $definition['fields'][$field_name]['length'] = '100'; } else { // Try use the provided length, if it doesn't exist default to 100. It's // not great but good enough for our dumps at this point. $definition['fields'][$field_name]['length'] = isset($matches[2]) ? $matches[2] : 100; } if (isset($row['Default'])) { $definition['fields'][$field_name]['default'] = $row['Default']; } if (isset($matches[4])) { $definition['fields'][$field_name]['unsigned'] = TRUE; } } $fields = Variable::export(array_keys($definition['fields']), ' '); if ($connection->driver() == 'mysql') { $definition['mysql_character_set'] = $character_sets[$table]; } $definition = Variable::export($definition, ' '); // Do our substitutions. $php = str_replace('{{TABLE}}', $table, $class_template); $php = str_replace('{{DRUPAL_VERSION}}', $version, $php); $php = str_replace('{{CLASS_NAME}}', $class, $php); $php = str_replace('{{PHP_VALUES}}', $values, $php); $php = str_replace('{{PHP_FIELDS}}', $fields, $php); $php = str_replace('{{TABLE_DEFINITION}}', $definition, $php); // Save the file. $php = implode("\n", array_map('rtrim', explode("\n", $php))); // Hash the dump code so that the restore script can easily determine if it // has been mucked with manually. $php .= '#' . md5($php) . "\n"; file_put_contents("$output_folder/$class.php", $php); } } elseif (isset($options['restore'])) { if (!\Drupal::moduleHandler()->moduleExists('migrate_drupal')) { echo "The migrate_drupal module must be enabled to restore a database.\n"; return; } elseif (empty($options['database'])) { echo "Missing required --database option.\n"; return; } $connection = Database::getConnection('default', $options['database']); $version = _get_core_version_from_options(); if ($version) { $tables_dir = DRUPAL_ROOT . '/core/modules/migrate_drupal/src/Tests/Table/' . $version; $tables = file_scan_directory($tables_dir, '/.php$/', array('recurse' => FALSE)); foreach ($tables as $table) { if (table_is_valid($table->uri)) { restore_table($table->uri, $connection); } else { echo "Skipping invalid table {$table->uri}\n"; } } } else { echo "Missing --core option.\n"; return; } } elseif (isset($options['validate'])) { $version = _get_core_version_from_options(); if ($version) { $tables_dir = DRUPAL_ROOT . '/core/modules/migrate_drupal/src/Tests/Table/' . $version; $tables = file_scan_directory($tables_dir, '/.php$/', array('recurse' => FALSE)); foreach ($tables as $table) { echo (table_is_valid($table->uri) ? 'OK' : 'INVALID') . ": {$table->uri}\n"; } } else { echo "Missing --core option.\n"; return; } } else { echo "Invalid options.\n"; return; } /** * Restores a table from a dump file. * * @param string $path * The path to the dump file. * @param \Drupal\Core\Database\Connection $connection * The target database connection. */ function restore_table($path, Connection $connection) { require_once $path; $version = _get_core_version_from_options(); $class = 'Drupal\migrate_drupal\Tests\Table\\' . $version . '\\' . substr(basename($path), 0, -4); try { (new $class($connection))->load(); } catch (\Exception $e) { echo 'ERROR: ' . $e->getMessage() . ' [' . get_class($e) . "]\n"; } } /** * Validates a dump file by reading in the MD5 of the file contents (last 32 bytes) * and comparing them with the MD5 of everything except those last 32 bytes. * * @param string $path * The path to the dump file. * * @return bool */ function table_is_valid($path) { // The call to rtrim() is important, since we need to extract a specific // number of bytes from the end of the file. $contents = rtrim(file_get_contents($path)); $dump = substr($contents, 0, -33); $hash = substr($contents, -32); return (md5($dump) === $hash); } /** * Statically maps the --core option to either 'd6' or 'd7', or NULL if the --core * option's value is unrecognized. * * @return string|null */ function _get_core_version_from_options() { global $options; if (isset($options['core'])) { switch ($options['core']) { case '6': case '6.x': return 'd6'; case '7': case '7.x': return 'd7'; default: break; } } } /** * Reads a Drupal 6 or 7 database to determine its major core version. * * @param \Drupal\Core\Database\Connection $connection * The database connection. * * @return string * * @throws \UnexpectedValueException if the discovered core version is unrecognized * or unsupported. */ function _get_core_version_from_database(Connection $connection) { $version = $connection ->select('system') ->fields('system', array('schema_version')) ->condition('name', 'system') ->execute() ->fetchField(); if ($version >= 7000) { return 'd7'; } elseif ($version >= 6000) { return 'd6'; } else { throw new \UnexpectedValueException("Unknown Drupal core version."); } } /** * Statically maps a SQL field type to a Schema API type. If there is no mapping, the * original field type is returned. * * @param string $sql_type * The field type as known to the database. * * @return string */ function _db_field_type_map($sql_type) { $map = array( 'longtext' => 'text', 'tinytext' => 'text', 'mediumtext' => 'text', 'tinyint' => 'int', 'smallint' => 'int', 'mediumint' => 'int', 'bigint' => 'int', 'int' => 'int', 'double' => 'numeric', 'float' => 'numeric', 'decimal' => 'numeric', 'longblob' => 'blob', ); return isset($map[$sql_type]) ? $map[$sql_type] : $sql_type; } /** * Returns the appropriate SQL query string to fetch all values from a table. * * @param string $table * The table's name. * * @return string */ function _db_get_query($table) { $queries = array( 'users' => 'SELECT * FROM {users} WHERE uid NOT IN (0,1)', // Volatile state variables should always be ignored. We don't want to // exclude all cache_* variables, since that would exclude cache_lifetime, // which is configuration, not state. 'variable' => "SELECT * FROM {variable} WHERE name NOT LIKE 'cache_flush_%' AND name NOT IN ('cache', 'drupal_css_cache_files', 'javascript_parsed', 'statistics_day_timestamp', 'update_last_check')", ); return isset($queries[$table]) ? $queries[$table] : 'SELECT * FROM {' . $table .'}'; }