Skip to content

Commit

Permalink
Refactor sql-sync extensions (drush-ops#2586)
Browse files Browse the repository at this point in the history
New sanitizers should be command files which implement \Drush\Commands\sql\SqlSanitizePluginInterface. You may use any Drupal API you please, instead of just returning SQL.
  • Loading branch information
weitzman authored Feb 4, 2017
1 parent e6a67c0 commit 66c433a
Show file tree
Hide file tree
Showing 16 changed files with 442 additions and 608 deletions.
197 changes: 0 additions & 197 deletions commands/sql/sql.drush.inc
Original file line number Diff line number Diff line change
Expand Up @@ -5,106 +5,6 @@
* Drush sql commands
*/

/**
* Implements hook_drush_help_alter().
*/
function sql_drush_help_alter(&$command) {
// Drupal 7+ only options.
if (drush_drupal_major_version() >= 7) {
if ($command['commandfile'] == 'sql') {
unset($command['options']['target']['hidden']);
}
}
}

/**
* Safely bootstrap Drupal to the point where we can
* access the database configuration.
*/
function drush_sql_bootstrap_database_configuration() {
// Under Drupal 7, if the database is configured but empty, then
// DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION will throw an exception.
// If this happens, we'll just catch it and continue.
// TODO: Fix this in the bootstrap, per http://drupal.org/node/1996004
try {
drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION);
}
catch (Exception $e) {
}
}

/**
* Check whether further bootstrap is needed. If so, do it.
*/
function drush_sql_bootstrap_further() {
if (!drush_get_option(array('db-url', 'db-spec'))) {
drush_sql_bootstrap_database_configuration();
}
}

// @todo Remove once sql-sync and archive-dump are using \Drush\Commands\OptionsCommands::optionsetTableSelection
function drush_sql_get_table_selection_options() {
return array(
'skip-tables-key' => 'A key in the $skip_tables array. @see example.drushrc.php. Optional.',
'structure-tables-key' => 'A key in the $structure_tables array. @see example.drushrc.php. Optional.',
'tables-key' => 'A key in the $tables array. Optional.',
'skip-tables-list' => 'A comma-separated list of tables to exclude completely. Optional.',
'structure-tables-list' => 'A comma-separated list of tables to include for structure, but not data. Optional.',
'tables-list' => 'A comma-separated list of tables to transfer. Optional.',
);
}

/**
* Call from a pre-sql-sync hook to register an sql
* query to be executed in the post-sql-sync hook.
* @see drush_sql_pre_sql_sync() and @see drush_sql_post_sql_sync().
*
* @param $id
* String containing an identifier representing this
* operation. This id is not actually used at the
* moment, it is just used to fufill the contract
* of drush contexts.
* @param $message
* String with the confirmation message that describes
* to the user what the post-sync operation is going
* to do. This confirmation message is printed out
* just before the user is asked whether or not the
* sql-sync operation should be continued.
* @param $query
* String containing the sql query to execute. If no
* query is provided, then the confirmation message will
* be displayed to the user, but no action will be taken
* in the post-sync hook. This is useful for drush modules
* that wish to provide their own post-sync hooks to fix
* up the target database in other ways (e.g. through
* Drupal APIs).
*/
function drush_sql_register_post_sync_op($id, $message, $query = NULL) {
$options = drush_get_context('post-sync-ops');

$options[$id] = array('message' => $message, 'query' => $query);

drush_set_context('post-sync-ops', $options);
}

/**
* Builds a confirmation message for all post-sync operations.
*
* @return string
* All post-sync operation messages concatenated together.
*/
function _drush_sql_get_post_sync_messages() {
$messages = '';
$operations = drush_get_context('post-sync-ops');
if (!empty($operations)) {
$messages = dt('The following operations will be done on the target database:') . "\n";

$bullets = array_column($operations, 'message');
$messages .= " * " . implode("\n * ", $bullets) . "\n";
}
return $messages;
}

/**
* Wrapper for drush_get_class; instantiates an driver-specific instance
* of SqlBase class.
Expand Down Expand Up @@ -157,100 +57,3 @@ function drush_sql_get_class($db_spec = NULL) {
function drush_sql_get_version() {
return drush_get_class('Drush\Sql\Sql', array(), array(drush_drupal_major_version())) ?: NULL;
}

/**
* Implements hook_drush_sql_sync_sanitize().
*
* Sanitize usernames, passwords, and sessions when the --sanitize option is used.
* It is also an example of how to write a database sanitizer for sql sync.
*
* To write your own sync hook function, define mymodule_drush_sql_sync_sanitize()
* in mymodule.drush.inc and follow the form of this function to add your own
* database sanitization operations via the register post-sync op function;
* @see drush_sql_register_post_sync_op(). This is the only thing that the
* sync hook function needs to do; sql-sync takes care of the rest.
*
* The function below has a lot of logic to process user preferences and
* generate the correct SQL regardless of whether Postgres, Mysql,
* Drupal 6/7/8 is in use. A simpler sanitize function that
* always used default values and only worked with Drupal 6 + mysql
* appears in the drush.api.php. @see hook_drush_sql_sync_sanitize().
*/
function sql_drush_sql_sync_sanitize($site) {
$site_settings = drush_sitealias_get_record($site);
$databases = sitealias_get_databases_from_record($site_settings);
$major_version = drush_drupal_major_version();
$wrap_table_name = (bool) drush_get_option('db-prefix');
$user_table_updates = array();
$message_list = array();

// Sanitize passwords.
$newpassword = drush_get_option(array('sanitize-password', 'destination-sanitize-password'), 'password');
if ($newpassword != 'no' && $newpassword !== 0) {
$pw_op = "";

// In Drupal 6, passwords are hashed via the MD5 algorithm.
if ($major_version == 6) {
$pw_op = "MD5('$newpassword')";
}
// In Drupal 7, passwords are hashed via a more complex algorithm,
// available via the user_hash_password function.
elseif ($major_version == 7) {
$core = DRUSH_DRUPAL_CORE;
include_once $core . '/includes/password.inc';
include_once $core . '/includes/bootstrap.inc';
$hash = user_hash_password($newpassword);
$pw_op = "'$hash'";
}
else {
// D8+. Mimic Drupal's /scripts/password-hash.sh
drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL);
$password_hasher = \Drupal::service('password');
$hash = $password_hasher->hash($newpassword);
$pw_op = "'$hash'";
}
if (!empty($pw_op)) {
$user_table_updates[] = "pass = $pw_op";
$message_list[] = "passwords";
}
}

// Sanitize email addresses.
$newemail = drush_get_option(array('sanitize-email', 'destination-sanitize-email'), 'user+%uid@localhost.localdomain');
if ($newemail != 'no' && $newemail !== 0) {
if (strpos($newemail, '%') !== FALSE) {
// We need a different sanitization query for Postgres and Mysql.

$db_driver = $databases['default']['default']['driver'];
if ($db_driver == 'pgsql') {
$email_map = array('%uid' => "' || uid || '", '%mail' => "' || replace(mail, '@', '_') || '", '%name' => "' || replace(name, ' ', '_') || '");
$newmail = "'" . str_replace(array_keys($email_map), array_values($email_map), $newemail) . "'";
}
elseif ($db_driver == 'mssql') {
$email_map = array('%uid' => "' + uid + '", '%mail' => "' + replace(mail, '@', '_') + '", '%name' => "' + replace(name, ' ', '_') + '");
$newmail = "'" . str_replace(array_keys($email_map), array_values($email_map), $newemail) . "'";
}
else {
$email_map = array('%uid' => "', uid, '", '%mail' => "', replace(mail, '@', '_'), '", '%name' => "', replace(name, ' ', '_'), '");
$newmail = "concat('" . str_replace(array_keys($email_map), array_values($email_map), $newemail) . "')";
}
$user_table_updates[] = "mail = $newmail, init = $newmail";
}
else {
$user_table_updates[] = "mail = '$newemail', init = '$newemail'";
}
$message_list[] = 'email addresses';
}

if (!empty($user_table_updates)) {
$table = $major_version >= 8 ? 'users_field_data' : 'users';
if ($wrap_table_name) {
$table = "{{$table}}";
}
$sanitize_query = "UPDATE {$table} SET " . implode(', ', $user_table_updates) . " WHERE uid > 0;";
drush_sql_register_post_sync_op('user-email', dt('Reset !message in !table table', array('!message' => implode(' and ', $message_list), '!table' => $table)), $sanitize_query);
}

$sanitizer = new \Drush\Commands\core\SanitizeCommands();
$sanitizer->doSanitize($major_version);
}
98 changes: 5 additions & 93 deletions drush.api.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,14 @@ function drush_hook_pre_pm_enable() {
}

/**
* Sql-sync sanitization example.
* Sql-sanitize example.
*
* This is equivalent to the built-in --sanitize option of sql-sync, but
* simplified to only work with default values on Drupal 6 + mysql.
* These plugins sanitize the DB, usually removing personal information.
*
* @see sql_drush_sql_sync_sanitize()
* @see \Drush\Commands\sql\SqlSanitizePluginInterface
*/
function hook_drush_sql_sync_sanitize($source) {
$table = drush_get_option('db-prefix') ? '{users}' : 'users';
drush_sql_register_post_sync_op('my-sanitize-id',
dt('Reset passwords and email addresses in user table.'),
"UPDATE $table SET pass = MD5('password'), mail = concat('user+', uid, '@localhost') WHERE uid > 0;");
function sanitize() {}
function messages() {}
}

/**
Expand Down Expand Up @@ -315,90 +311,6 @@ function hook_drush_cache_clear(&$types, $include_bootstrapped_types) {
$types['views'] = 'views_invalidate_cache';
}

/**
* Inform drush about one or more engine types.
*
* This hook allow to declare available engine types, the cli option to select
* between engine implementatins, which one to use by default, global options
* and other parameters. Commands may override this info when declaring the
* engines they use.
*
* @return array
* An array whose keys are engine type names and whose values describe
* the characteristics of the engine type in relation to command definitions:
*
* - description: The engine type description.
* - topic: If specified, the name of the topic command that will
* display the automatically generated topic for this engine.
* - topic-file: If specified, the path to the file that will be
* displayed at the head of the automatically generated topic for
* this engine. This path is relative to the Drush root directory;
* non-core commandfiles should therefore use:
* 'topic-file' => dirname(__FILE__) . '/mytopic.html';
* - topics: If set, contains a list of topics that should be added to
* the "Topics" section of any command that uses this engine. Note
* that if 'topic' is set, it will automatically be added to the topics
* list, and therefore does not need to also be listed here.
* - option: The command line option to choose an implementation for
* this engine type.
* FALSE means there's no option. That is, the engine type is for internal
* usage of the command and thus an implementation is not selectable.
* - default: The default implementation to use by the engine type.
* - options: Engine options common to all implementations.
* - add-options-to-command: If there's a single implementation for this
* engine type, add its options as command level options.
* - combine-help: If there are multiple implementations for this engine
* type, then instead of adding multiple help items in the form of
* --engine-option=engine-type [description], instead combine all help
* options into a single --engine-option that lists the different possible
* values that can be used.
*
* @see drush_get_engine_types_info()
* @see pm_drush_engine_type_info()
*/
function hook_drush_engine_type_info() {
return array(
'dessert' => array(
'description' => 'Choose a dessert while the sandwich is baked.',
'option' => 'dessert',
'default' => 'ice-cream',
'options' => 'sweetness',
'add-options-to-command' => FALSE,
),
);
}

/**
* Inform drush about one or more engines implementing a given engine type.
*
* - description: The engine implementation's description.
* - implemented-by: The engine that actually implements this engine.
* This is useful to allow the implementation of similar engines
* in the reference one.
* Defaults to the engine type key (e.g. 'ice-cream').
* - verbose-only: The engine implementation will only appear in help
* output in --verbose mode.
*
* This hook allow to declare implementations for an engine type.
*
* @see pm_drush_engine_package_handler()
* @see pm_drush_engine_version_control()
*/
function hook_drush_engine_ENGINE_TYPE() {
return array(
'ice-cream' => array(
'description' => 'Feature rich ice-cream with all kind of additives.',
'options' => array(
'flavour' => 'Choose your favorite flavour',
),
),
'frozen-yogurt' => array(
'description' => 'Frozen dairy dessert made with yogurt instead of milk and cream.',
'implemented-by' => 'ice-cream',
),
);
}

/**
* Alter the order that hooks are invoked.
*
Expand Down
18 changes: 0 additions & 18 deletions includes/drupal.inc
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,3 @@ function _drush_drupal_parse_info_file($data, $merge_item = NULL) {
function drush_cid_install_profile() {
return drush_get_cid('install_profile', array(), array(drush_get_context('DRUSH_SELECTED_DRUPAL_SITE_CONF_PATH')));
}

/*
* An array of options shared by sql-sanitize and sql-sync commands.
*/
function drupal_sanitize_options() {
return array(
'sanitize-password' => array(
'description' => 'The password to assign to all accounts in the sanitization operation, or "no" to keep passwords unchanged.',
'example-value' => 'password',
'value' => 'required',
),
'sanitize-email' => array(
'description' => 'The pattern for test email addresses in the sanitization operation, or "no" to keep email addresses unchanged. May contain replacement patterns %uid, %mail or %name.',
'example-value' => 'user+%uid@localhost',
'value' => 'required',
),
);
}
Loading

0 comments on commit 66c433a

Please sign in to comment.