2015-08-18 00:00:26 +00:00
#!/usr/bin/env php
<?php
/**
* This script is designed for assisting core developers working on Migrate to
* regenerate the Migrate dump files. Technically this script can be used to
* dump any database into a PHP representation, but that' s not its primary
* use case .
*
* Dump files only need to be updated when you' re adding or updating tests which
* need new Drupal source data. Drupal 6 and 7 are supported by this script. The
* version of Drupal will be auto-detected during dumping by scanning the system
* table' s schema_version column.
*
* To dump a database, you must have a connection to it defined in settings.php.
* Then you can run this script like so:
* migrate-db.sh --dump --database= CONNECTION_KEY
*
* To restore a Drupal 6 database from dump files:
* migrate-db.sh --restore --core= 6 --database= CONNECTION_KEY
*
* And to restore a Drupal 7 DB:
* migrate-db.sh --restore --core= 7 --database= CONNECTION_KEY
*
* You can also validate a set of dumps to ensure that they haven' t been altered.
* For Drupal 6 and 7, respectively:
* migrate-db.sh --validate --core= 6
* migrate-db.sh --validate --core= 7
*
* --dump and --restore always require the --database option. --validate and --restore
* always require the --core option, which can accept values of 6, 6.x, 7, or 7.x.
*/
use Doctrine\C ommon\I nflector\I nflector;
use Drupal\C ore\D atabase\D atabase;
use Drupal\C ore\D atabase\C onnection;
use Drupal\C omponent\U tility\V ariable;
use Drupal\C ore\D rupalKernel;
use Drupal\C ore\S ite\S ettings;
use Symfony\C omponent\H ttpFoundation\R equest;
if ( PHP_SAPI != = 'cli' ) {
return ;
}
$autoloader = require __DIR__ . '/../../autoload.php' ;
require_once __DIR__ . '/../includes/bootstrap.inc' ;
$request = Request::createFromGlobals( ) ;
Settings::initialize( dirname( dirname( __DIR__) ) , DrupalKernel::findSitePath( $request ) , $autoloader ) ;
// Fully bootstrap Drupal so that things like file_scan_directory( ) can be used
// ( for validating and restoring, and possibly other things) .
$kernel = DrupalKernel::createFromRequest( $request , $autoloader , 'prod' ) ;
$kernel ->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 = ' <?php
/**
* @file
* Contains \D rupal\m igrate_drupal\T ests\T able\{ { DRUPAL_VERSION} } \{ { CLASS_NAME} } .
*
* THIS IS A GENERATED FILE. DO NOT EDIT.
*
* @see core/scripts/migrate-db.sh
* @see https://www.drupal.org/sandbox/benjy/2405029
*/
namespace Drupal\m igrate_drupal\T ests\T able\{ { DRUPAL_VERSION} } ;
use Drupal\m igrate_drupal\T ests\D ump\D rupalDumpBase;
/**
* Generated file to represent the { { TABLE} } table.
*/
class { { CLASS_NAME} } extends DrupalDumpBase {
public function load( ) {
$this ->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 ( !\D rupal::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 \D rupal\C ore\D atabase\C onnection $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 ( \E xception $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.
*
2015-08-27 19:03:05 +00:00
* @return bool
2015-08-18 00:00:26 +00:00
*/
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 \D rupal\C ore\D atabase\C onnection $connection
* The database connection.
*
* @return string
*
* @throws \U nexpectedValueException 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 \U nexpectedValueException( "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 .'}' ;
}