-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert sql:sanitize command. Convert the UserTable sanitizer to a Li…
…stener
- Loading branch information
Showing
6 changed files
with
198 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Drush\Commands\sql\sanitize; | ||
|
||
use Consolidation\AnnotatedCommand\Events\CustomEventAwareInterface; | ||
use Consolidation\AnnotatedCommand\Events\CustomEventAwareTrait; | ||
use Drush\Attributes as CLI; | ||
use Drush\Boot\DrupalBootLevels; | ||
use Drush\Commands\AutowireTrait; | ||
use Drush\Commands\core\DocsCommands; | ||
use Drush\Event\SanitizeConfirmsEvent; | ||
use Drush\Exceptions\UserAbortException; | ||
use Drush\Style\DrushStyle; | ||
use Psr\EventDispatcher\EventDispatcherInterface; | ||
use Symfony\Component\Console\Attribute\AsCommand; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
#[AsCommand( | ||
name: self::NAME, | ||
description: 'Sanitize the database by removing or obfuscating user data.', | ||
aliases: ['sqlsan','sql-sanitize'] | ||
)] | ||
// @todo Deal with topics on classes. | ||
#[CLI\Topics(topics: [DocsCommands::HOOKS])] | ||
#[CLI\Bootstrap(level: DrupalBootLevels::FULL)] | ||
final class SanitizeCommand extends Command implements CustomEventAwareInterface | ||
{ | ||
use AutowireTrait; | ||
use CustomEventAwareTrait; | ||
|
||
const NAME = 'sql:sanitize'; | ||
|
||
public function __construct(protected EventDispatcherInterface $eventDispatcher) | ||
{ | ||
parent::__construct(); | ||
} | ||
|
||
|
||
protected function configure() | ||
{ | ||
$this | ||
->setDescription('Sanitize the database by removing or obfuscating user data.') | ||
->addUsage('drush sql:sanitize --sanitize-password=no') | ||
->addUsage('drush sql:sanitize --allowlist-fields=field_biography,field_phone_number'); | ||
} | ||
|
||
/** | ||
* Commandfiles may add custom operations by implementing a Listener that subscribes to two events: | ||
* | ||
* - `\Drush\Events\SanitizeConfirmsEvent`. Display summary to user before confirmation. | ||
* - `\Symfony\Component\Console\Event\ConsoleTerminateEvent`. Run queries or call APIs to perform sanitizing | ||
* | ||
* Several working Listeners may be found at https://github.com/drush-ops/drush/tree/13.x/src/Drush/Listeners/sanitize | ||
*/ | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
$io = new DrushStyle($input, $output); | ||
|
||
/** | ||
* In order to present only one prompt, collect all confirmations up front. | ||
*/ | ||
$event = new SanitizeConfirmsEvent($input); | ||
$this->eventDispatcher->dispatch($event, SanitizeConfirmsEvent::class); | ||
$messages = $event->getMessages(); | ||
|
||
// Also collect from legacy commandfiles. | ||
$handlers = $this->getCustomEventHandlers(SanitizeCommands::CONFIRMS); | ||
foreach ($handlers as $handler) { | ||
$handler($messages, $input); | ||
} | ||
// @phpstan-ignore if.alwaysFalse | ||
if ($messages) { | ||
$output->writeln(dt('The following operations will be performed:')); | ||
$io->listing($messages); | ||
} | ||
if (!$io->confirm(dt('Do you want to sanitize the current database?'))) { | ||
throw new UserAbortException(); | ||
} | ||
// All sanitize operations happen during the built-in console.terminate event. | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php | ||
|
||
namespace Drush\Event; | ||
|
||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Contracts\EventDispatcher\Event; | ||
|
||
/* | ||
* A custom event, for prompting the user about candidate sanitize operations. | ||
* | ||
* Listeners should add their confirm messages via addMessage(). | ||
*/ | ||
|
||
final class SanitizeConfirmsEvent extends Event | ||
{ | ||
public function __construct( | ||
protected InputInterface $input, | ||
protected array $messages = [], | ||
) { | ||
} | ||
|
||
public function setInput(InputInterface $input): void | ||
{ | ||
$this->input = $input; | ||
} | ||
|
||
public function getInput(): InputInterface | ||
{ | ||
return $this->input; | ||
} | ||
|
||
public function addMessage(string $message): self | ||
{ | ||
$this->messages[] = $message; | ||
return $this; | ||
} | ||
|
||
public function getMessages(): array | ||
{ | ||
return $this->messages; | ||
} | ||
|
||
public function setMessages(array $messages): self | ||
{ | ||
$this->messages = $messages; | ||
return $this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,44 +2,76 @@ | |
|
||
declare(strict_types=1); | ||
|
||
namespace Drush\Commands\sql\sanitize; | ||
namespace Drush\Listeners\sanitize; | ||
|
||
use Drupal\Core\Database\Connection; | ||
use Drupal\Core\Database\Query\SelectInterface; | ||
use Consolidation\AnnotatedCommand\CommandData; | ||
use Consolidation\AnnotatedCommand\Hooks\HookManager; | ||
use Drupal\Core\Entity\EntityTypeManagerInterface; | ||
use Drupal\Core\Password\PasswordInterface; | ||
use Drush\Attributes as CLI; | ||
use Drush\Commands\AutowireTrait; | ||
use Drush\Commands\DrushCommands; | ||
use Drush\Commands\sql\sanitize\SanitizeCommand; | ||
use Drush\Event\ConsoleDefinitionsEvent; | ||
use Drush\Event\SanitizeConfirmsEvent; | ||
use Drush\Sql\SqlBase; | ||
use Drush\Utils\StringUtils; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Psr\Log\LoggerInterface; | ||
use Symfony\Component\Console\Event\ConsoleTerminateEvent; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener; | ||
|
||
/** | ||
* A sql:sanitize plugin. | ||
* Sanitize emails and passwords. This also an example of how to write a | ||
* database sanitizer for sql:sync. | ||
*/ | ||
final class SanitizeUserTableCommands extends DrushCommands implements SanitizePluginInterface | ||
#[AsEventListener(method: 'onDefinition')] | ||
#[AsEventListener(method: 'onSanitizeConfirm')] | ||
#[AsEventListener(method: 'onConsoleTerminate')] | ||
final class SanitizeUserTableListener | ||
{ | ||
use AutowireTrait; | ||
|
||
public function __construct( | ||
protected Connection $database, | ||
protected PasswordInterface $passwordHasher, | ||
protected EntityTypeManagerInterface $entityTypeManager | ||
protected EntityTypeManagerInterface $entityTypeManager, | ||
protected LoggerInterface $logger, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
/** | ||
* Sanitize emails and passwords. This also an example of how to write a | ||
* database sanitizer for sql:sync. | ||
*/ | ||
#[CLI\Hook(type: HookManager::POST_COMMAND_HOOK, target: SanitizeCommands::SANITIZE)] | ||
public function sanitize($result, CommandData $commandData): void | ||
public function onDefinition(ConsoleDefinitionsEvent $event): void | ||
{ | ||
foreach ($event->getApplication()->all() as $id => $command) { | ||
if ($command->getName() === SanitizeCommand::NAME) { | ||
$command->addOption( | ||
'sanitize-email', | ||
null, | ||
InputOption::VALUE_REQUIRED, | ||
'The pattern for test email addresses in the sanitization operation, or <info>no</info> to keep email addresses unchanged. May contain replacement patterns <info>%uid</info>, <info>%mail</info> or <info>%name</info>.', | ||
'user+%[email protected]' | ||
) | ||
->addOption('sanitize-password', null, InputOption::VALUE_REQUIRED, 'By default, passwords are randomized. Specify <info>no</info> to disable that. Specify any other value to set all passwords to that value.') | ||
->addOption('ignored-roles', null, InputOption::VALUE_REQUIRED, 'A comma delimited list of roles. Users with at least one of the roles will be exempt from sanitization.'); | ||
} | ||
} | ||
} | ||
|
||
public function onSanitizeConfirm(SanitizeConfirmsEvent $event): void | ||
{ | ||
$options = $event->getInput()->getOptions(); | ||
if ($this->isEnabled($options['sanitize-password'])) { | ||
$event->addMessage(dt('Sanitize user passwords.')); | ||
} | ||
if ($this->isEnabled($options['sanitize-email'])) { | ||
$event->addMessage(dt('Sanitize user emails.')); | ||
} | ||
if (in_array('ignored-roles', $options)) { | ||
$event->addMessage(dt('Preserve user emails and passwords for the specified roles.')); | ||
} | ||
} | ||
|
||
public function onConsoleTerminate(ConsoleTerminateEvent $event): void | ||
{ | ||
$options = $commandData->options(); | ||
$options = $event->getInput()->getOptions(); | ||
$query = $this->database->update('users_field_data')->condition('uid', 0, '>'); | ||
$messages = []; | ||
|
||
|
@@ -60,7 +92,7 @@ public function sanitize($result, CommandData $commandData): void | |
if ($this->isEnabled($options['sanitize-email'])) { | ||
if (str_contains($options['sanitize-email'], '%')) { | ||
// We need a different sanitization query for MSSQL, Postgres and Mysql. | ||
$sql = SqlBase::create($commandData->input()->getOptions()); | ||
$sql = SqlBase::create($event->getInput()->getOptions()); | ||
$db_driver = $sql->scheme(); | ||
if ($db_driver === 'pgsql') { | ||
$email_map = ['%uid' => "' || uid || '", '%mail' => "' || replace(mail, '@', '_') || '", '%name' => "' || replace(name, ' ', '_') || '"]; | ||
|
@@ -100,34 +132,11 @@ public function sanitize($result, CommandData $commandData): void | |
$query->execute(); | ||
$this->entityTypeManager->getStorage('user')->resetCache(); | ||
foreach ($messages as $message) { | ||
$this->logger()->success($message); | ||
$this->logger->success($message); | ||
} | ||
} | ||
} | ||
|
||
#[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 <info>no</info> to keep email addresses unchanged. May contain replacement patterns <info>%uid</info>, <info>%mail</info> or <info>%name</info>.')] | ||
#[CLI\Option(name: 'sanitize-password', description: 'By default, passwords are randomized. Specify <info>no</info> to disable that. Specify any other value to set all passwords to that value.')] | ||
#[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+%[email protected]', 'sanitize-password' => null, 'ignored-roles' => null]): void | ||
{ | ||
} | ||
|
||
#[CLI\Hook(type: HookManager::ON_EVENT, target: SanitizeCommands::CONFIRMS)] | ||
public function messages(&$messages, InputInterface $input): void | ||
{ | ||
$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.'); | ||
} | ||
if (in_array('ignored-roles', $options)) { | ||
$messages[] = dt('Preserve user emails and passwords for the specified roles.'); | ||
} | ||
} | ||
|
||
/** | ||
* Test an option value to see if it is disabled. | ||
*/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters