From ce708064ebb89aa1132c903913d6971195fda289 Mon Sep 17 00:00:00 2001 From: Primoz Hmeljak Date: Tue, 9 Jul 2024 16:55:45 +0200 Subject: [PATCH 1/2] Sanitize user names on sql-sanitize. --- .../sanitize/SanitizeUserTableCommands.php | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php b/src/Commands/sql/sanitize/SanitizeUserTableCommands.php index bd2a18fef4..7580cff485 100644 --- a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php +++ b/src/Commands/sql/sanitize/SanitizeUserTableCommands.php @@ -8,7 +8,10 @@ use Drupal\Core\Database\Query\SelectInterface; use Consolidation\AnnotatedCommand\CommandData; use Consolidation\AnnotatedCommand\Hooks\HookManager; +use Drupal\Core\Database\Query\Update; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\Core\Password\PasswordInterface; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; @@ -27,7 +30,8 @@ final class SanitizeUserTableCommands extends DrushCommands implements SanitizeP public function __construct( protected Connection $database, protected PasswordInterface $passwordHasher, - protected EntityTypeManagerInterface $entityTypeManager + protected EntityTypeManagerInterface $entityTypeManager, + protected EntityFieldManagerInterface $entityFieldManager ) { parent::__construct(); } @@ -80,6 +84,20 @@ public function sanitize($result, CommandData $commandData): void $messages[] = dt('User emails sanitized.'); } + // Sanitize username. + if ($this->isEnabled($options['sanitize-username'])) { + [$name_table, $name_column] = $this->getFieldTableDetails('user', 'name'); + [$uid_table, $uid_column] = $this->getFieldTableDetails('user', 'uid'); + assert($uid_table === $name_table); + + // Updates usernames to the pattern user_%uid. + $query + ->condition($uid_column, 0, '>') + ->expression($name_column, "CONCAT('user_', $uid_column)"); + + $messages[] = dt("Usernames sanitized."); + } + if (!empty($options['ignored-roles'])) { $roles = explode(',', $options['ignored-roles']); /** @var SelectInterface $roles_query */ @@ -108,8 +126,9 @@ public function sanitize($result, CommandData $commandData): void #[CLI\Hook(type: HookManager::OPTION_HOOK, target: SanitizeCommands::SANITIZE)] #[CLI\Option(name: 'sanitize-email', 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.')] #[CLI\Option(name: 'sanitize-password', description: 'By default, passwords are randomized. Specify no to disable that. Specify any other value to set all passwords to that value.')] + #[CLI\Option(name: 'sanitize-username', description: 'Sanitizes usernames replacing the originals with user_UID.')] #[CLI\Option(name: 'ignored-roles', description: 'A comma delimited list of roles. Users with at least one of the roles will be exempt from sanitization.')] - public function options($options = ['sanitize-email' => 'user+%uid@localhost.localdomain', 'sanitize-password' => null, 'ignored-roles' => null]): void + public function options($options = ['sanitize-email' => 'user+%uid@localhost.localdomain', 'sanitize-password' => null, 'sanitize-username' => 'no', 'ignored-roles' => null]): void { } @@ -123,11 +142,44 @@ public function messages(&$messages, InputInterface $input): void if ($this->isEnabled($options['sanitize-email'])) { $messages[] = dt('Sanitize user emails.'); } + if ($this->isEnabled($options['sanitize-username'])) { + $messages[] = dt('Sanitize usernames.'); + } if (in_array('ignored-roles', $options)) { $messages[] = dt('Preserve user emails and passwords for the specified roles.'); } } + /** + * Gets database details for a given field. + * + * It returns the field table name and main property column name. + * + * @param string $entity_type_id + * The entity type ID the field's attached to. + * @param string $field_name + * The field name. + * + * @return array + * An indexed array, containing: + * - the table name; + * - the column name. + */ + protected function getFieldTableDetails(string $entity_type_id, string $field_name): array { + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$storage instanceof SqlEntityStorageInterface) { + $context = ['!entity_type_id' => $entity_type_id]; + throw new \Exception(dt("Unable to get !entity_type_id table mapping details, its storage doesn't implement \Drupal\Core\Entity\Sql\SqlEntityStorageInterface.", $context)); + } + $mapping = $storage->getTableMapping(); + $table = $mapping->getFieldTableName($field_name); + $columns = $mapping->getColumnNames($field_name); + $definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id); + $main_property = $definitions[$field_name]->getMainPropertyName(); + + return [$table, $columns[$main_property]]; + } + /** * Test an option value to see if it is disabled. */ From 129dffe3f74d63e145662325a00c17279a986e22 Mon Sep 17 00:00:00 2001 From: Primoz Hmeljak Date: Tue, 9 Jul 2024 17:03:59 +0200 Subject: [PATCH 2/2] Sanitize user names: fix phpcs errors. --- src/Commands/sql/sanitize/SanitizeUserTableCommands.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php b/src/Commands/sql/sanitize/SanitizeUserTableCommands.php index 7580cff485..97766d9b72 100644 --- a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php +++ b/src/Commands/sql/sanitize/SanitizeUserTableCommands.php @@ -165,7 +165,8 @@ public function messages(&$messages, InputInterface $input): void * - the table name; * - the column name. */ - protected function getFieldTableDetails(string $entity_type_id, string $field_name): array { + protected function getFieldTableDetails(string $entity_type_id, string $field_name): array + { $storage = $this->entityTypeManager->getStorage($entity_type_id); if (!$storage instanceof SqlEntityStorageInterface) { $context = ['!entity_type_id' => $entity_type_id];