diff --git a/src/Commands/sql/sanitize/SanitizeUserTableCommands.php b/src/Commands/sql/sanitize/SanitizeUserTableCommands.php index bd2a18fef4..97766d9b72 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,45 @@ 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. */