diff --git a/commands/sql/sql.drush.inc b/commands/sql/sql.drush.inc index 558478caab..8811a329d0 100644 --- a/commands/sql/sql.drush.inc +++ b/commands/sql/sql.drush.inc @@ -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. @@ -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); -} diff --git a/drush.api.php b/drush.api.php index 21a9aa8684..3b711953f3 100644 --- a/drush.api.php +++ b/drush.api.php @@ -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() {} } /** @@ -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. * diff --git a/includes/drupal.inc b/includes/drupal.inc index 19248e60de..25b07a17b4 100644 --- a/includes/drupal.inc +++ b/includes/drupal.inc @@ -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', - ), - ); -} diff --git a/lib/Drush/Commands/core/SanitizeCommands.php b/lib/Drush/Commands/core/SanitizeCommands.php deleted file mode 100644 index 566fb25573..0000000000 --- a/lib/Drush/Commands/core/SanitizeCommands.php +++ /dev/null @@ -1,256 +0,0 @@ -wrap to TRUE if a db-prefix is set with drush. - */ - protected function setWrap() { - $this->wrap = $wrap_table_name = (bool) drush_get_option('db-prefix'); - } - - - /** - * Sanitize the database by removing or obfuscating user data. - * - * Commandfiles may add custom operations by implementing hook_drush_sql_sync_sanitize(). - * - * @command sql-sanitize - * - * @bootstrap DRUSH_BOOTSTRAP_NONE - * @description Run sanitization operations on the current database. - * @option db-prefix Enable replacement of braces in sanitize queries. - * @option db-url A Drupal 6 style database URL. E.g., - * mysql://root:pass@127.0.0.1/db - * @option sanitize-email 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 - * @option sanitize-password The password to assign to all accounts in the - * sanitization operation, or "no" to keep passwords unchanged. Example - * value: password - * @option whitelist-fields A comma delimited list of fields exempt from sanitization. - * @aliases sqlsan - * @usage drush sql-sanitize --sanitize-password=no - * Sanitize database without modifying any passwords. - * @usage drush sql-sanitize --whitelist-fields=field_biography,field_phone_number - * Sanitizes database but exempts two user fields from modification. - * @see hook_drush_sql_sync_sanitize() for adding custom sanitize routines. - */ - public function sqlSanitize($options = [ - 'db-prefix' => FALSE, - 'db-url' => '', - 'sanitize-email' => '', - 'sanitize-password' => '', - 'whitelist-fields' => '', - ]) { - drush_sql_bootstrap_further(); - if ($options['db-prefix']) { - drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_DATABASE); - } - - // Drush itself implements this via sql_drush_sql_sync_sanitize(). - drush_command_invoke_all('drush_sql_sync_sanitize', 'default'); - $operations = drush_get_context('post-sync-ops'); - if (!empty($operations)) { - if (!drush_get_context('DRUSH_SIMULATE')) { - $messages = _drush_sql_get_post_sync_messages(); - if ($messages) { - drush_print(); - drush_print($messages); - } - } - $queries = array_column($operations, 'query'); - $sanitize_query = implode(" ", $queries); - } - if (!drush_confirm(dt('Do you really want to sanitize the current database?'))) { - return drush_user_abort(); - } - - if ($sanitize_query) { - $sql = drush_sql_get_class(); - $sanitize_query = $sql->query_prefix($sanitize_query); - $result = $sql->query($sanitize_query); - if (!$result) { - throw new \Exception(dt('Sanitize query failed.')); - } - } - } - - /** - * Performs database sanitization. - * - * @param int $major_version - * E.g., 6, 7, or 8. - */ - public function doSanitize($major_version) { - $this->setWrap(); - $this->sanitizeSessions(); - - if ($major_version == 8) { - $this->sanitizeComments(); - $this->sanitizeUserFields(); - } - } - - /** - * Sanitize string fields associated with the user. - * - * We've got to do a good bit of SQL-foo here because Drupal services are - * not yet available. - */ - public function sanitizeUserFields() { - /** @var SqlBase $sql_class */ - $sql_class = drush_sql_get_class(); - $tables = $sql_class->listTables(); - $whitelist_fields = (array) explode(',', drush_get_option('whitelist-fields')); - - foreach ($tables as $table) { - if (strpos($table, 'user__field_') === 0) { - $field_name = substr($table, 6, strlen($table)); - if (in_array($field_name, $whitelist_fields)) { - continue; - } - - $output = $this->query("SELECT data FROM config WHERE name = 'field.field.user.user.$field_name';"); - $field_config = unserialize($output[0]); - $field_type = $field_config['field_type']; - $randomizer = new Random(); - - switch ($field_type) { - - case 'email': - $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->name(10) . '@example.com'); - break; - - case 'string': - $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->name(255)); - break; - - case 'string_long': - $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->sentences(1)); - break; - - case 'telephone': - $this->sanitizeTableColumn($table, $field_name . '_value', '15555555555'); - break; - - case 'text': - $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->paragraphs(2)); - break; - - case 'text_long': - $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->paragraphs(10)); - break; - - case 'text_with_summary': - $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->paragraphs(2)); - $this->sanitizeTableColumn($table, $field_name . '_summary', $randomizer->name(255)); - break; - } - } - } - } - - /** - * Replaces all values in given table column with the specified value. - * - * @param string $table - * The database table name. - * @param string $column - * The database column to be updated. - * @param $value - * The new value. - */ - public function sanitizeTableColumn($table, $column, $value) { - $table_name_wrapped = $this->wrapTableName($table); - $sql = "UPDATE $table_name_wrapped SET $column='$value';"; - drush_sql_register_post_sync_op($table.$column, dt("Replaces all values in $table table with the same random long string."), $sql); - } - - /** - * Truncates the session table. - */ - public function sanitizeSessions() { - // Seems quite portable (SQLite?) - http://en.wikipedia.org/wiki/Truncate_(SQL) - $table_name = $this->wrapTableName('sessions'); - $sql_sessions = "TRUNCATE TABLE $table_name;"; - drush_sql_register_post_sync_op('sessions', dt('Truncate Drupal\'s sessions table'), $sql_sessions); - } - - /** - * Sanitizes comments_field_data table. - */ - public function sanitizeComments() { - - $comments_enabled = $this->query("SHOW TABLES LIKE 'comment_field_data';"); - if (!$comments_enabled) { - return; - } - - $comments_table = $this->wrapTableName('comment_field_data'); - $sql_comments = "UPDATE $comments_table SET name='Anonymous', mail='', homepage='http://example.com' WHERE uid = 0;"; - drush_sql_register_post_sync_op('anon_comments', dt('Remove names and email addresses from anonymous user comments.'), $sql_comments); - - $sql_comments = "UPDATE $comments_table SET name=CONCAT('User', `uid`), mail=CONCAT('user+', `uid`, '@example.com'), homepage='http://example.com' WHERE uid <> 0;"; - drush_sql_register_post_sync_op('auth_comments', dt('Replace names and email addresses from authenticated user comments.'), $sql_comments); - } - - /** - * Wraps a table name in brackets if a database prefix is being used. - * - * @param string $table_name - * The name of the database table. - * - * @return string - * The (possibly wrapped) table name. - */ - public function wrapTableName($table_name) { - if ($this->wrap) { - $processed = '{' . $table_name . '}'; - } - else { - $processed = $table_name; - } - - return $processed; - } - - /** - * Executes a sql command using drush sqlq and returns the output. - * - * @param string $query - * The SQL query to execute. Must end in a semicolon! - * - * @return string - * The output of the query. - */ - protected function query($query) { - $current = drush_get_context('DRUSH_SIMULATE'); - drush_set_context('DRUSH_SIMULATE', FALSE); - $sql = drush_sql_get_class(); - $success = $sql->query($query); - $output = drush_shell_exec_output(); - drush_set_context('DRUSH_SIMULATE', $current); - - return $output; - } - -} - diff --git a/lib/Drush/Commands/sql/SanitizeCommands.php b/lib/Drush/Commands/sql/SanitizeCommands.php new file mode 100644 index 0000000000..c22f9b5eea --- /dev/null +++ b/lib/Drush/Commands/sql/SanitizeCommands.php @@ -0,0 +1,70 @@ + FALSE, 'db-url' => '', 'sanitize-email' => 'user+%uid@localhost.localdomain', 'sanitize-password' => 'password', 'whitelist-fields' => '']) { + /** + * In order to present only one prompt, collect all confirmations from + * commandfiles up front. sql-sanitize plugins are commandfiles that implement + * \Drush\Commands\sql\SanitizePluginInterface + */ + $messages = []; + $input = $this->input(); + $handlers = $this->getCustomEventHandlers('sql-sanitize-confirms'); + foreach ($handlers as $handler) { + $handler($messages, $input); + } + if (!empty($messages)) { + drush_print(dt('The following operations will be performed:')); + foreach ($messages as $message) { + drush_print('* '. $message); + } + } + if (!drush_confirm(dt('Do you really want to sanitize the current database?'))) { + return drush_user_abort(); + } + + // All sanitize operations defined in post-command hooks, including Drush + // core sanitize routines. See \Drush\Commands\sql\SanitizePluginInterface. + } +} + diff --git a/lib/Drush/Commands/sql/SanitizeCommentsCommands.php b/lib/Drush/Commands/sql/SanitizeCommentsCommands.php new file mode 100644 index 0000000000..53bcdcbf4e --- /dev/null +++ b/lib/Drush/Commands/sql/SanitizeCommentsCommands.php @@ -0,0 +1,60 @@ +applies()) { + //Update anon. + Database::getConnection()->update('comment_field_data') + ->fields([ + 'name' => 'Anonymous', + 'mail' => '', + 'homepage' => 'http://example.com' + ]) + ->condition('uid', 0) + ->execute(); + + // Update auth. + Database::getConnection()->update('comment_field_data') + ->expression('name', "CONCAT('User', `uid`)") + ->expression('mail', "CONCAT('user+', `uid`, '@example.com')") + ->fields(['homepage' => 'http://example.com']) + ->condition('uid', 1, '>=') + ->execute(); + $this->logger()->success(dt('Comment display names and emails removed.')); + } + } + + /** + * @hook on-event sql-sanitize-confirms + * + * @inheritdoc + */ + public function messages(&$messages, InputInterface $input) { + if ($this->applies()) { + $messages[] = dt('Remove comment display names and emails.'); + } + } + + protected function applies() { + drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL); + return \Drupal::moduleHandler()->moduleExists('comment'); + } +} + diff --git a/lib/Drush/Commands/sql/SanitizeSessionsCommands.php b/lib/Drush/Commands/sql/SanitizeSessionsCommands.php new file mode 100644 index 0000000000..6028c6cadf --- /dev/null +++ b/lib/Drush/Commands/sql/SanitizeSessionsCommands.php @@ -0,0 +1,35 @@ +truncate('sessions')->execute(); + $this->logger()->success(dt('Sessions table truncated.')); + } + + /** + * @hook on-event sql-sanitize-confirms + * + * @inheritdoc + */ + public function messages(&$messages, InputInterface $input) { + $messages[] = dt('Truncate sessions table.'); + } +} + diff --git a/lib/Drush/Commands/sql/SanitizeUserFieldsCommands.php b/lib/Drush/Commands/sql/SanitizeUserFieldsCommands.php new file mode 100644 index 0000000000..dbf6724134 --- /dev/null +++ b/lib/Drush/Commands/sql/SanitizeUserFieldsCommands.php @@ -0,0 +1,99 @@ +options(); + $randomizer = new Random(); + $conn = Database::getConnection(); + $field_definitions = \Drupal::entityManager()->getFieldDefinitions('user', 'user'); + $field_storage = \Drupal::entityManager()->getFieldStorageDefinitions('user'); + foreach (explode(',', $options['whitelist-fields']) as $key => $def) { + unset($field_definitions[$key], $field_storage[$key]); + } + + foreach ($field_definitions as $key => $def) { + $execute = FALSE; + if ($field_storage[$key]->isBaseField()) { + continue; + } + + $table = 'user__' . $key; + $query = $conn->update($table); + $name = $def->getName(); + $field_type_class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($def->getType()); + $value_array = $field_type_class::generateSampleValue($def); + $value = $value_array['value']; + switch ($def->getType()) { + case 'email': + $query->fields([$name . '_value' => $value]); + $execute = TRUE; + break; + case 'string': + $query->fields([$name . '_value' => $value]); + $execute = TRUE; + break; + + case 'string_long': + $query->fields([$name . '_value' => $value]); + $execute = TRUE; + break; + + case 'telephone': + $query->fields([$name . '_value' => '15555555555']); + $execute = TRUE; + break; + + case 'text': + $query->fields([$name . '_value' => $value]); + $execute = TRUE; + break; + + case 'text_long': + $query->fields([$name . '_value' => $value]); + $execute = TRUE; + break; + + case 'text_with_summary': + $query->fields([ + $name . '_value' => $value, + $name . '_summary' => $value_array['summary'], + ]); + $execute = TRUE; + break; + } + if ($execute) { + $query->execute(); + $this->logger()->success(dt('!table table sanitized.', ['!table' => $table])); + } + } + } + + /** + * @hook on-event sql-sanitize-confirms + * + * @inheritdoc + */ + public function messages(&$messages, InputInterface $input) { + $messages[] = dt('Sanitize text Fields associated with the user.'); + } +} \ No newline at end of file diff --git a/lib/Drush/Commands/sql/SanitizeUserTableCommands.php b/lib/Drush/Commands/sql/SanitizeUserTableCommands.php new file mode 100644 index 0000000000..769c99f285 --- /dev/null +++ b/lib/Drush/Commands/sql/SanitizeUserTableCommands.php @@ -0,0 +1,96 @@ +options(); + $query = Database::getConnection()->update('users_field_data') + ->condition('uid', 0, '>'); + $messages = []; + + // Sanitize passwords. + if ($this->isEnabled($options['sanitize-password'])) { + // D8+. Mimic Drupal's /scripts/password-hash.sh + drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL); + $password_hasher = \Drupal::service('password'); + $hash = $password_hasher->hash($options['sanitize-password']); + $query->fields(['pass' => $hash]); + $messages[] = dt('User passwords sanitized.'); + } + + // Sanitize email addresses. + if ($this->isEnabled($options['sanitize-email'])) { + if (strpos($options['sanitize-email'], '%') !== FALSE) { + // We need a different sanitization query for MSSQL, Postgres and Mysql. + $sql = drush_sql_get_class(); + $db_driver = $sql->scheme(); + if ($db_driver == 'pgsql') { + $email_map = array('%uid' => "' || uid || '", '%mail' => "' || replace(mail, '@', '_') || '", '%name' => "' || replace(name, ' ', '_') || '"); + $new_mail = "'" . str_replace(array_keys($email_map), array_values($email_map), $options['sanitize-email']) . "'"; + } + elseif ($db_driver == 'mssql') { + $email_map = array('%uid' => "' + uid + '", '%mail' => "' + replace(mail, '@', '_') + '", '%name' => "' + replace(name, ' ', '_') + '"); + $new_mail = "'" . str_replace(array_keys($email_map), array_values($email_map), $options['sanitize-email']) . "'"; + } + else { + $email_map = array('%uid' => "', uid, '", '%mail' => "', replace(mail, '@', '_'), '", '%name' => "', replace(name, ' ', '_'), '"); + $new_mail = "concat('" . str_replace(array_keys($email_map), array_values($email_map), $options['sanitize-email']) . "')"; + } + $query->expression('mail', $new_mail); + } + else { + $query->fields(['mail' => $options['sanitize-email']]); + } + $messages[] = dt('User emails sanitized.'); + } + + if ($messages) { + $query->execute(); + foreach ($messages as $message) { + $this->logger()->success($message); + } + } + } + + /** + * @hook on-event sql-sanitize-confirms + * + * @inheritdoc + */ + public function messages(&$messages, InputInterface $input) { + $options = $input->getOptions(); + if ($this->isEnabled($options['sanitize-password'])) { + $messages[] = dt('Sanitize user passwords.'); + } + if ($this->isEnabled($options['sanitize-email'])) { + $messages[] = dt('Sanitize user emails.'); + } + } + + /** + * Test an option value to see if it is disabled. + * @param $value + * @return bool + */ + protected function isEnabled($value) { + return $value != 'no' && $value != '0'; + } +} + diff --git a/lib/Drush/Commands/sql/SqlCommands.php b/lib/Drush/Commands/sql/SqlCommands.php index 5b736e8d71..8426ff282c 100644 --- a/lib/Drush/Commands/sql/SqlCommands.php +++ b/lib/Drush/Commands/sql/SqlCommands.php @@ -15,7 +15,7 @@ class SqlCommands extends DrushCommands { * @hidden */ public function conf($options = ['format' => 'yaml', 'all' => FALSE, 'show-passwords' => FALSE]) { - drush_sql_bootstrap_database_configuration(); + drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); if ($options['all']) { $sqlVersion = drush_sql_get_version(); $return = $sqlVersion->getAll(); @@ -49,7 +49,7 @@ public function conf($options = ['format' => 'yaml', 'all' => FALSE, 'show-passw * Fish: Import SQL statements from a file into the current database. */ public function connect($options = ['extra' => '']) { - drush_sql_bootstrap_further(); + $this->further($options); $sql = drush_sql_get_class(); return $sql->connect(FALSE); } @@ -69,7 +69,7 @@ public function connect($options = ['extra' => '']) { * Create the database as specified in the db-url option. */ public function create() { - drush_sql_bootstrap_further(); + $this->further($options); $sql = drush_sql_get_class(); $db_spec = $sql->db_spec(); // Prompt for confirmation. @@ -95,7 +95,7 @@ public function create() { * @topics docs-policy */ public function drop() { - drush_sql_bootstrap_further(); + $this->further($options); $sql = drush_sql_get_class(); $db_spec = $sql->db_spec(); if (!drush_confirm(dt('Do you really want to drop all tables in the database !db?', array('!db' => $db_spec['database'])))) { @@ -119,7 +119,7 @@ public function drop() { * @remote-tty */ public function cli() { - drush_sql_bootstrap_further(); + $this->further($options); $sql = drush_sql_get_class(); drush_shell_proc_open($sql->connect()); } @@ -149,7 +149,7 @@ public function cli() { * */ public function query($query = '', $options = ['result-file' => NULL, 'file' => NULL, 'extra' => NULL, 'db-prefix' => NULL, 'db-spec' => NULL]) { - drush_sql_bootstrap_further(); + $this->further($options); $filename = $options['file']; // Enable prefix processing when db-prefix option is used. if ($options['db-prefix']) { @@ -196,8 +196,17 @@ public function query($query = '', $options = ['result-file' => NULL, 'file' => * @hidden-option create-db */ public function dump($options = ['result-file' => NULL, 'create-db' => NULL, 'data-only' => NULL, 'ordered-dump' => NULL, 'gzip' => NULL, 'extra' => NULL, 'extra-dump' => NULL]) { - drush_sql_bootstrap_further(); + $this->further($options); $sql = drush_sql_get_class(); return $sql->dump($options); } + + /** + * Check whether further bootstrap is needed. If so, do it. + */ + public function further($options) { + if (empty($options['db-url']) && empty($options['db-spec'])) { + drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); + } + } } \ No newline at end of file diff --git a/lib/Drush/Commands/sql/SqlSanitizePluginInterface.php b/lib/Drush/Commands/sql/SqlSanitizePluginInterface.php new file mode 100644 index 0000000000..048aa54a54 --- /dev/null +++ b/lib/Drush/Commands/sql/SqlSanitizePluginInterface.php @@ -0,0 +1,30 @@ += 8) { - // Add user fields and a test User. - $this->drush('pm-enable', array('field,text,telephone,comment'), $options + array('yes' => NULL)); - $this->drush('php-script', array( - 'user_fields-D' . UNISH_DRUPAL_MAJOR_VERSION, - $name, - $mail + // Add user fields and a test User. + $this->drush('pm-enable', array('field,text,telephone,comment'), $options + array('yes' => NULL)); + $this->drush('php-script', array( + 'user_fields-D' . UNISH_DRUPAL_MAJOR_VERSION, + $name, + $mail ), $options + array( - 'script-path' => __DIR__ . '/resources', - 'debug' => NULL - )); - } + 'script-path' => __DIR__ . '/resources', + ) + ); // Copy stage to dev with --sanitize. $sync_options = array( @@ -68,8 +66,8 @@ public function localSqlSync() { ); $this->drush('sql-sync', array('@stage', '@dev'), $sync_options); - // Confirm that the sample user has the correct email address on the staging site - $this->drush('user-information', array($name), $options + array('format' => 'csv', 'include-field-labels' => 0, 'strict' => 0)); + // Confirm that the sample user is unchanged on the staging site + $this->drush('user-information', array($name), $options + array('format' => 'csv', 'include-field-labels' => 0, 'strict' => 0), '@stage'); $output = $this->getOutput(); $row = str_getcsv($output); $uid = $row[0]; @@ -89,9 +87,6 @@ public function localSqlSync() { $this->assertEquals("user+$uid@localhost.localdomain", $row[2], 'email address was sanitized on destination site.'); $this->assertEquals($name, $row[1]); - // @todo Confirm that the role_permissions table no longer exists in dev site (i.e. wildcard expansion works in sql-sync). - // $this->drush('sql-query', array('SELECT * FROM role_permission'), $options, NULL, NULL, self::EXIT_ERROR); - // Copy stage to dev with --sanitize and a fixed sanitized email $sync_options = array( 'sanitize' => NULL, @@ -115,23 +110,22 @@ public function localSqlSync() { $this->assertEquals("user@mysite.org", $row[2], 'email address was sanitized (fixed email) on destination site.'); $this->assertEquals($name, $row[1]); - if (UNISH_DRUPAL_MAJOR_VERSION >= 8) { - $fields = [ - 'field_user_email' => 'joe.user.alt@myhome.com', - 'field_user_string' => 'Private info', - 'field_user_string_long' => 'Really private info', - 'field_user_text' => 'Super private info', - 'field_user_text_long' => 'Super duper private info', - 'field_user_text_with_summary' => 'Private', - ]; - // Assert that field DO NOT contain values. - foreach ($fields as $field_name => $value) { - $this->assertUserFieldContents($field_name, $value, $options); - } - - // Assert that field_user_telephone DOES contain "5555555555". - $this->assertUserFieldContents('field_user_telephone', '5555555555', $options, TRUE); + + $fields = [ + 'field_user_email' => 'joe.user.alt@myhome.com', + 'field_user_string' => 'Private info', + 'field_user_string_long' => 'Really private info', + 'field_user_text' => 'Super private info', + 'field_user_text_long' => 'Super duper private info', + 'field_user_text_with_summary' => 'Private', + ]; + // Assert that field DO NOT contain values. + foreach ($fields as $field_name => $value) { + $this->assertUserFieldContents($field_name, $value, $options); } + + // Assert that field_user_telephone DOES contain "5555555555". + $this->assertUserFieldContents('field_user_telephone', '5555555555', $options, TRUE); } /**