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.
*/