From 8a9429efa4b0a459a657a44b41a596969878ad65 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sat, 26 Mar 2016 12:35:38 -0700
Subject: [PATCH 01/12] [ticket/14561] User delete command

PHPBB3-14561
---
 .../default/container/services_console.yml    |  12 ++
 phpBB/language/en/cli.php                     |   4 +
 phpBB/phpbb/console/command/user/delete.php   | 175 ++++++++++++++++++
 tests/console/user/delete_test.php            | 169 +++++++++++++++++
 4 files changed, 360 insertions(+)
 create mode 100644 phpBB/phpbb/console/command/user/delete.php
 create mode 100644 tests/console/user/delete_test.php

diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml
index 0a28c0ed1f..3ada9d1639 100644
--- a/phpBB/config/default/container/services_console.yml
+++ b/phpBB/config/default/container/services_console.yml
@@ -232,3 +232,15 @@ services:
             - '%core.php_ext%'
         tags:
             - { name: console.command }
+
+    console.command.user.delete:
+        class: phpbb\console\command\user\delete
+        arguments:
+            - '@user'
+            - '@dbal.conn'
+            - '@language'
+            - '@log'
+            - '%core.root_path%'
+            - '%core.php_ext%'
+        tags:
+            - { name: console.command }
diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php
index 6cb516ebfd..bb7baf67f7 100644
--- a/phpBB/language/en/cli.php
+++ b/phpBB/language/en/cli.php
@@ -87,6 +87,9 @@ $lang = array_merge($lang, array(
 	'CLI_DESCRIPTION_USER_ADD_OPTION_PASSWORD'	=> 'Password of the new user',
 	'CLI_DESCRIPTION_USER_ADD_OPTION_EMAIL'		=> 'E-mail address of the new user',
 	'CLI_DESCRIPTION_USER_ADD_OPTION_NOTIFY'	=> 'Send account activation email to the new user (not sent by default)',
+	'CLI_DESCRIPTION_USER_DELETE'				=> 'Delete a user account.',
+	'CLI_DESCRIPTION_USER_DELETE_USERNAME'		=> 'Username of the user to delete',
+	'CLI_DESCRIPTION_USER_DELETE_OPTION_POSTS'	=> 'Delete all posts by the user. Without this option, the user’s posts will be retained.',
 
 	'CLI_EXTENSION_DISABLE_FAILURE'		=> 'Could not disable extension %s',
 	'CLI_EXTENSION_DISABLE_SUCCESS'		=> 'Successfully disabled extension %s',
@@ -126,6 +129,7 @@ $lang = array_merge($lang, array(
 	'CLI_THUMBNAIL_NOTHING_TO_DELETE'	=> 'No thumbnails to delete.',
 
 	'CLI_USER_ADD_SUCCESS'		=> 'Successfully added user %s.',
+	'CLI_USER_DELETE_CONFIRM'	=> 'Are you sure you want to delete ‘%s’? [y/N]',
 ));
 
 // Additional help for commands.
diff --git a/phpBB/phpbb/console/command/user/delete.php b/phpBB/phpbb/console/command/user/delete.php
new file mode 100644
index 0000000000..360b119e17
--- /dev/null
+++ b/phpBB/phpbb/console/command/user/delete.php
@@ -0,0 +1,175 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\console\command\user;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+class delete extends \phpbb\console\command\command
+{
+	/** @var \phpbb\db\driver\driver_interface */
+	protected $db;
+
+	/** @var \phpbb\language\language */
+	protected $language;
+
+	/** @var \phpbb\log\log_interface */
+	protected $log;
+
+	/**
+	 * phpBB root path
+	 *
+	 * @var string
+	 */
+	protected $phpbb_root_path;
+
+	/**
+	 * PHP extension.
+	 *
+	 * @var string
+	 */
+	protected $php_ext;
+
+	/**
+	 * Construct method
+	 *
+	 * @param \phpbb\user                       $user
+	 * @param \phpbb\db\driver\driver_interface $db
+	 * @param \phpbb\language\language          $language
+	 * @param \phpbb\log\log_interface          $log
+	 * @param string                            $phpbb_root_path
+	 * @param string                            $php_ext
+	 */
+	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\language\language $language, \phpbb\log\log_interface $log, $phpbb_root_path, $php_ext)
+	{
+		$this->db = $db;
+		$this->language = $language;
+		$this->log = $log;
+		$this->phpbb_root_path = $phpbb_root_path;
+		$this->php_ext = $php_ext;
+
+		$this->language->add_lang('acp/users');
+		parent::__construct($user);
+	}
+
+	/**
+	 * Sets the command name and description
+	 *
+	 * @return null
+	 */
+	protected function configure()
+	{
+		$this
+			->setName('user:delete')
+			->setDescription($this->language->lang('CLI_DESCRIPTION_USER_DELETE'))
+			->addArgument(
+				'username',
+				InputArgument::REQUIRED,
+				$this->language->lang('CLI_DESCRIPTION_USER_DELETE_USERNAME')
+			)
+			->addOption(
+				'delete-posts',
+				null,
+				InputOption::VALUE_NONE,
+				$this->language->lang('CLI_DESCRIPTION_USER_DELETE_OPTION_POSTS')
+			)
+		;
+	}
+
+	/**
+	 * Executes the command user:delete
+	 *
+	 * Deletes a user from the database. An option to delete the user's posts
+	 * is available, by default posts will be retained.
+	 *
+	 * @param InputInterface  $input  The input stream used to get the options
+	 * @param OutputInterface $output The output stream, used to print messages
+	 *
+	 * @return int 0 if all is well, 1 if any errors occurred
+	 */
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		$name = $input->getArgument('username');
+		$mode = ($input->getOption('delete-posts')) ? 'remove' : 'retain';
+
+		if ($name)
+		{
+			$io = new SymfonyStyle($input, $output);
+
+			if (!$user_row = $this->get_user_data($name))
+			{
+				$io->error($this->language->lang('NO_USER'));
+				return 1;
+			}
+
+			if (!function_exists('user_delete'))
+			{
+				require($this->phpbb_root_path . 'includes/functions_user.' . $this->php_ext);
+			}
+
+			user_delete($mode, $user_row['user_id'], $user_row['username']);
+
+			$this->log->add('admin', ANONYMOUS, '', 'LOG_USER_DELETED', false, array($user_row['username']));
+
+			$io->success($this->language->lang('USER_DELETED'));
+		}
+
+		return 0;
+	}
+
+	/**
+	 * Interacts with the user.
+	 * Confirm they really want to delete the account...last chance!
+	 *
+	 * @param InputInterface  $input  An InputInterface instance
+	 * @param OutputInterface $output An OutputInterface instance
+	 */
+	protected function interact(InputInterface $input, OutputInterface $output)
+	{
+		$helper = $this->getHelper('question');
+
+		$question = new ConfirmationQuestion(
+			$this->language->lang('CLI_USER_DELETE_CONFIRM', $input->getArgument('username')),
+			false
+		);
+
+		if (!$helper->ask($input, $output, $question))
+		{
+			$input->setArgument('username', false);
+		}
+	}
+
+	/**
+	 * Get the user's data from the database
+	 *
+	 * @param string $name A user name
+	 * @return mixed The user's id and username if they exist, false otherwise.
+	 */
+	protected function get_user_data($name)
+	{
+		$sql = 'SELECT user_id, username
+			FROM ' . USERS_TABLE . "
+			WHERE username_clean = '" . $this->db->sql_escape(utf8_clean_string($name)) . "'";
+		$result = $this->db->sql_query_limit($sql, 1);
+		$user_row = $this->db->sql_fetchrow($result);
+		$this->db->sql_freeresult($result);
+
+		return $user_row;
+	}
+}
diff --git a/tests/console/user/delete_test.php b/tests/console/user/delete_test.php
new file mode 100644
index 0000000000..5162358713
--- /dev/null
+++ b/tests/console/user/delete_test.php
@@ -0,0 +1,169 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\CommandTester;
+use phpbb\console\command\user\delete;
+
+class phpbb_console_command_user_delete_test extends phpbb_database_test_case
+{
+	protected $db;
+	protected $user;
+	protected $language;
+	protected $log;
+	protected $command_name;
+	protected $question;
+	protected $phpbb_root_path;
+	protected $php_ext;
+
+	public function getDataSet()
+	{
+		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
+	}
+
+	public function setUp()
+	{
+		global $db, $cache, $config, $user, $phpbb_dispatcher, $phpbb_container, $phpbb_root_path, $phpEx;
+
+		$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
+		$phpbb_container = new phpbb_mock_container_builder();
+		$phpbb_container->set('cache.driver', new phpbb_mock_cache());
+		$phpbb_container->set('notification_manager', new phpbb_mock_notification_manager());
+
+		$cache = $phpbb_container->get('cache.driver');
+
+		$config = new \phpbb\config\config(array());
+
+		$db = $this->db = $this->new_dbal();
+
+		$this->language = $this->getMockBuilder('\phpbb\language\language')
+			->disableOriginalConstructor()
+			->getMock();
+		$this->language->expects($this->any())
+			->method('lang')
+			->will($this->returnArgument(0));
+		$user = $this->user = $this->getMock('\phpbb\user', array(), array(
+			$this->language,
+			'\phpbb\datetime'
+		));
+
+		$this->log = $this->getMockBuilder('\phpbb\log\log')
+			->disableOriginalConstructor()
+			->getMock();
+
+		$phpbb_container->set('auth.provider.db', new phpbb_mock_auth_provider());
+		$provider_collection = new \phpbb\auth\provider_collection($phpbb_container, $config);
+		$provider_collection->add('auth.provider.db');
+		$phpbb_container->set(
+			'auth.provider_collection',
+			$provider_collection
+		);
+
+		$this->phpbb_root_path = $phpbb_root_path;
+		$this->php_ext = $phpEx;
+
+		parent::setUp();
+	}
+
+	public function test_delete()
+	{
+		$command_tester = $this->get_command_tester();
+
+		$this->assertEquals(3, $this->get_user_id('Test'));
+
+		$this->question->setInputStream($this->getInputStream("yes\n"));
+
+		$command_tester->execute(array(
+			'command'			=> $this->command_name,
+			'username'			=> 'Test',
+			'--delete-posts'	=> false,
+		));
+
+		$this->assertNull($this->get_user_id('Test'));
+		$this->assertContains('USER_DELETED', $command_tester->getDisplay());
+	}
+
+	public function test_delete_non_user()
+	{
+		$command_tester = $this->get_command_tester();
+
+		$this->assertNull($this->get_user_id('Foo'));
+
+		$this->question->setInputStream($this->getInputStream("yes\n"));
+
+		$command_tester->execute(array(
+			'command'			=> $this->command_name,
+			'username'			=> 'Foo',
+			'--delete-posts'	=> false,
+		));
+
+		$this->assertContains('NO_USER', $command_tester->getDisplay());
+	}
+
+	public function test_delete_cancel()
+	{
+		$command_tester = $this->get_command_tester();
+
+		$this->assertEquals(3, $this->get_user_id('Test'));
+
+		$this->question->setInputStream($this->getInputStream("no\n"));
+
+		$command_tester->execute(array(
+			'command'			=> $this->command_name,
+			'username'			=> 'Test',
+			'--delete-posts'	=> false,
+		));
+
+		$this->assertNotNull($this->get_user_id('Test'));
+	}
+
+	public function get_command_tester()
+	{
+		$application = new Application();
+		$application->add(new delete(
+			$this->user,
+			$this->db,
+			$this->language,
+			$this->log,
+			$this->phpbb_root_path,
+			$this->php_ext
+		));
+
+		$command = $application->find('user:delete');
+		$this->command_name = $command->getName();
+		$this->question = $command->getHelper('question');
+
+		return new CommandTester($command);
+	}
+
+	public function get_user_id($username)
+	{
+		$sql = 'SELECT user_id
+			FROM ' . USERS_TABLE . '
+			WHERE ' . 'username = ' . "'" . $username . "'";
+		$result = $this->db->sql_query($sql);
+		$row = $this->db->sql_fetchrow($result);
+		$this->db->sql_freeresult($result);
+
+		return $row['user_id'];
+	}
+
+	public function getInputStream($input)
+	{
+		$stream = fopen('php://memory', 'r+', false);
+		fputs($stream, $input);
+		rewind($stream);
+
+		return $stream;
+	}
+}

From 91f1116e046818fb49a19ff59652f684c6f5f736 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sat, 26 Mar 2016 12:37:27 -0700
Subject: [PATCH 02/12] [ticket/14561] User activate command

PHPBB3-14561
---
 .../default/container/services_console.yml    |  14 ++
 phpBB/language/en/cli.php                     |   7 +
 phpBB/phpbb/console/command/user/activate.php | 223 ++++++++++++++++++
 tests/console/user/activate_test.php          | 124 ++++++++++
 4 files changed, 368 insertions(+)
 create mode 100644 phpBB/phpbb/console/command/user/activate.php
 create mode 100644 tests/console/user/activate_test.php

diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml
index 3ada9d1639..710487dfe8 100644
--- a/phpBB/config/default/container/services_console.yml
+++ b/phpBB/config/default/container/services_console.yml
@@ -220,6 +220,20 @@ services:
         tags:
             - { name: console.command }
 
+    console.command.user.activate:
+        class: phpbb\console\command\user\activate
+        arguments:
+            - '@user'
+            - '@dbal.conn'
+            - '@config'
+            - '@language'
+            - '@log'
+            - '@notification_manager'
+            - '%core.root_path%'
+            - '%core.php_ext%'
+        tags:
+            - { name: console.command }
+
     console.command.user.add:
         class: phpbb\console\command\user\add
         arguments:
diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php
index bb7baf67f7..d76993e3bf 100644
--- a/phpBB/language/en/cli.php
+++ b/phpBB/language/en/cli.php
@@ -82,6 +82,11 @@ $lang = array_merge($lang, array(
 	'CLI_DESCRIPTION_THUMBNAIL_GENERATE'	=> 'Generate all missing thumbnails.',
 	'CLI_DESCRIPTION_THUMBNAIL_RECREATE'	=> 'Recreate all thumbnails.',
 
+	'CLI_DESCRIPTION_USER_ACTIVATE'				=> 'Activate (or deactivate) a user account.',
+	'CLI_DESCRIPTION_USER_ACTIVATE_USERNAME'	=> 'Username of the account to activate.',
+	'CLI_DESCRIPTION_USER_ACTIVATE_DEACTIVATE'	=> 'Deactivate the user’s account',
+	'CLI_DESCRIPTION_USER_ACTIVATE_ACTIVE'		=> 'The user is already active.',
+	'CLI_DESCRIPTION_USER_ACTIVATE_INACTIVE'	=> 'The user is already inactive.',
 	'CLI_DESCRIPTION_USER_ADD'					=> 'Add a new user.',
 	'CLI_DESCRIPTION_USER_ADD_OPTION_USERNAME'	=> 'Username of the new user',
 	'CLI_DESCRIPTION_USER_ADD_OPTION_PASSWORD'	=> 'Password of the new user',
@@ -135,6 +140,8 @@ $lang = array_merge($lang, array(
 // Additional help for commands.
 $lang = array_merge($lang, array(
 	'CLI_HELP_CRON_RUN'			=> $lang['CLI_DESCRIPTION_CRON_RUN'] . ' Optionally you can specify a cron task name to run only the specified cron task.',
+	'CLI_HELP_USER_ACTIVATE'	=> 'Activate a user account, or deactivate an account using the <info>--deactivate</info> option.
+To optionally send an activation email to the user, use the <info>--send-email</info> option.',
 	'CLI_HELP_USER_ADD'			=> 'The <info>%command.name%</info> command adds a new user:
 If this command is run without options, you will be prompted to enter them.
 To optionally send an email to the new user, use the <info>--send-email</info> option.',
diff --git a/phpBB/phpbb/console/command/user/activate.php b/phpBB/phpbb/console/command/user/activate.php
new file mode 100644
index 0000000000..890827afb6
--- /dev/null
+++ b/phpBB/phpbb/console/command/user/activate.php
@@ -0,0 +1,223 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\console\command\user;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+class activate extends \phpbb\console\command\command
+{
+	/** @var \phpbb\db\driver\driver_interface */
+	protected $db;
+
+	/** @var \phpbb\config\config */
+	protected $config;
+
+	/** @var \phpbb\language\language */
+	protected $language;
+
+	/** @var \phpbb\log\log_interface */
+	protected $log;
+
+	/** @var \phpbb\notification\manager */
+	protected $notifications;
+
+	/**
+	 * phpBB root path
+	 *
+	 * @var string
+	 */
+	protected $phpbb_root_path;
+
+	/**
+	 * PHP extension.
+	 *
+	 * @var string
+	 */
+	protected $php_ext;
+
+	/**
+	 * Construct method
+	 *
+	 * @param \phpbb\user                       $user
+	 * @param \phpbb\db\driver\driver_interface $db
+	 * @param \phpbb\config\config              $config
+	 * @param \phpbb\language\language          $language
+	 * @param \phpbb\log\log_interface          $log
+	 * @param \phpbb\notification\manager       $notifications
+	 * @param string                            $phpbb_root_path
+	 * @param string                            $php_ext
+	 */
+	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\language\language $language, \phpbb\log\log_interface $log, \phpbb\notification\manager $notifications, $phpbb_root_path, $php_ext)
+	{
+		$this->db = $db;
+		$this->config = $config;
+		$this->language = $language;
+		$this->log = $log;
+		$this->notifications = $notifications;
+		$this->phpbb_root_path = $phpbb_root_path;
+		$this->php_ext = $php_ext;
+
+		$this->language->add_lang('acp/users');
+		parent::__construct($user);
+	}
+
+	/**
+	 * Sets the command name and description
+	 *
+	 * @return null
+	 */
+	protected function configure()
+	{
+		$this
+			->setName('user:activate')
+			->setDescription($this->language->lang('CLI_DESCRIPTION_USER_ACTIVATE'))
+			->setHelp($this->language->lang('CLI_HELP_USER_ACTIVATE'))
+			->addArgument(
+				'username',
+				InputArgument::REQUIRED,
+				$this->language->lang('CLI_DESCRIPTION_USER_ACTIVATE_USERNAME')
+			)
+			->addOption(
+				'deactivate',
+				'd',
+				InputOption::VALUE_NONE,
+				$this->language->lang('CLI_DESCRIPTION_USER_ACTIVATE_DEACTIVATE')
+			)
+			->addOption(
+				'send-email',
+				null,
+				InputOption::VALUE_NONE,
+				$this->language->lang('CLI_DESCRIPTION_USER_ADD_OPTION_NOTIFY')
+			)
+		;
+	}
+
+	/**
+	 * Executes the command user:activate
+	 *
+	 * Deletes a user from the database. An option to delete the user's posts
+	 * is available, by default posts will be retained.
+	 *
+	 * @param InputInterface  $input  The input stream used to get the options
+	 * @param OutputInterface $output The output stream, used to print messages
+	 *
+	 * @return int 0 if all is well, 1 if any errors occurred
+	 */
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		$io = new SymfonyStyle($input, $output);
+
+		$name = $input->getArgument('username');
+		$mode = ($input->getOption('deactivate')) ? 'deactivate' : 'activate';
+
+		if (!$user_row = $this->get_user_data($name))
+		{
+			$io->error($this->language->lang('NO_USER'));
+			return 1;
+		}
+
+		// Check if the user is already active (or inactive)
+		if ($mode == 'activate' && $user_row['user_type'] != USER_INACTIVE)
+		{
+			$io->error($this->language->lang('CLI_DESCRIPTION_USER_ACTIVATE_ACTIVE'));
+			return 1;
+		}
+		else if ($mode == 'deactivate' && $user_row['user_type'] == USER_INACTIVE)
+		{
+			$io->error($this->language->lang('CLI_DESCRIPTION_USER_ACTIVATE_INACTIVE'));
+			return 1;
+		}
+
+		// Activate the user account
+		if (!function_exists('user_active_flip'))
+		{
+			require($this->phpbb_root_path . 'includes/functions_user.' . $this->php_ext);
+		}
+
+		user_active_flip($mode, $user_row['user_id']);
+
+		// Notify the user upon activation
+		if ($mode == 'activate' && $this->config['require_activation'] == USER_ACTIVATION_ADMIN)
+		{
+			$this->send_notification($user_row, $input);
+		}
+
+		// Log and display the result
+		$msg = ($mode == 'activate') ? 'USER_ADMIN_ACTIVATED' : 'USER_ADMIN_DEACTIVED';
+		$log = ($mode == 'activate') ? 'LOG_USER_ACTIVE' : 'LOG_USER_INACTIVE';
+
+		$this->log->add('admin', ANONYMOUS, '', $log, false, array($user_row['username']));
+		$this->log->add('user', ANONYMOUS, '', $log . '_USER', false, array(
+			'reportee_id' => $user_row['user_id']
+		));
+
+		$io->success($this->language->lang($msg));
+
+		return 0;
+	}
+
+	/**
+	 * Send account activation notification to user
+	 *
+	 * @param array           $user_row The user data array
+	 * @param InputInterface  $input    The input stream used to get the options
+	 * @return null
+	 */
+	protected function send_notification($user_row, InputInterface $input)
+	{
+		$this->notifications->delete_notifications('notification.type.admin_activate_user', $user_row['user_id']);
+
+		if ($input->getOption('send-email'))
+		{
+			if (!class_exists('messenger'))
+			{
+				require($this->phpbb_root_path . 'includes/functions_messenger.' . $this->php_ext);
+			}
+
+			$messenger = new \messenger(false);
+			$messenger->template('admin_welcome_activated', $user_row['user_lang']);
+			$messenger->set_addresses($user_row);
+			$messenger->anti_abuse_headers($this->config, $this->user);
+			$messenger->assign_vars(array(
+					'USERNAME'	=> htmlspecialchars_decode($user_row['username']))
+			);
+
+			$messenger->send(NOTIFY_EMAIL);
+		}
+	}
+
+	/**
+	 * Get the user's data from the database
+	 *
+	 * @param string $name A user name
+	 * @return mixed The user's data array if they exist, false otherwise.
+	 */
+	protected function get_user_data($name)
+	{
+		$sql = 'SELECT *
+			FROM ' . USERS_TABLE . "
+			WHERE username_clean = '" . $this->db->sql_escape(utf8_clean_string($name)) . "'";
+		$result = $this->db->sql_query_limit($sql, 1);
+		$user_row = $this->db->sql_fetchrow($result);
+		$this->db->sql_freeresult($result);
+
+		return $user_row;
+	}
+}
diff --git a/tests/console/user/activate_test.php b/tests/console/user/activate_test.php
new file mode 100644
index 0000000000..90a4d74a26
--- /dev/null
+++ b/tests/console/user/activate_test.php
@@ -0,0 +1,124 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\CommandTester;
+use phpbb\console\command\user\activate;
+
+class phpbb_console_command_user_activate_test extends phpbb_database_test_case
+{
+	protected $db;
+	protected $config;
+	protected $user;
+	protected $language;
+	protected $log;
+	protected $notifications;
+	protected $command_name;
+	protected $phpbb_root_path;
+	protected $php_ext;
+
+	public function getDataSet()
+	{
+		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
+	}
+
+	public function setUp()
+	{
+		global $config, $db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, $phpEx;
+
+		$auth = $this->getMock('\phpbb\auth\auth');
+
+		$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
+
+		$config = $this->config = new \phpbb\config\config(array());
+
+		$db = $this->db = $this->new_dbal();
+
+		$this->language = $this->getMockBuilder('\phpbb\language\language')
+			->disableOriginalConstructor()
+			->getMock();
+		$this->language->expects($this->any())
+			->method('lang')
+			->will($this->returnArgument(0));
+		$user = $this->user = $this->getMock('\phpbb\user', array(), array(
+			$this->language,
+			'\phpbb\datetime'
+		));
+
+		$this->log = $this->getMockBuilder('\phpbb\log\log')
+			->disableOriginalConstructor()
+			->getMock();
+
+		$this->notifications = $this->getMockBuilder('\phpbb\notification\manager')
+			->disableOriginalConstructor()
+			->getMock();
+
+		$this->phpbb_root_path = $phpbb_root_path;
+		$this->php_ext = $phpEx;
+
+		parent::setUp();
+	}
+
+	public function activate_test_data()
+	{
+		return array(
+			// Test an inactive user
+			array('Test', false, 'USER_ADMIN_ACTIVATED'),
+			array('Test', true, 'CLI_DESCRIPTION_USER_ACTIVATE_INACTIVE'),
+
+			// Test an active user
+			array('Test 2', false, 'CLI_DESCRIPTION_USER_ACTIVATE_ACTIVE'),
+			array('Test 2', true, 'USER_ADMIN_DEACTIVED'),
+
+			// Test a non existent user
+			array('Foo', false, 'NO_USER'),
+			array('Foo', true, 'NO_USER'),
+		);
+	}
+
+	/**
+	 * @dataProvider activate_test_data
+	 */
+	public function test_activate($username, $deactivate, $expected)
+	{
+		$command_tester = $this->get_command_tester();
+
+		$command_tester->execute(array(
+			'command'		=> $this->command_name,
+			'username'		=> $username,
+			'--deactivate'	=> $deactivate,
+		));
+
+		$this->assertContains($expected, $command_tester->getDisplay());
+	}
+
+	public function get_command_tester()
+	{
+		$application = new Application();
+		$application->add(new activate(
+			$this->user,
+			$this->db,
+			$this->config,
+			$this->language,
+			$this->log,
+			$this->notifications,
+			$this->phpbb_root_path,
+			$this->php_ext
+		));
+
+		$command = $application->find('user:activate');
+		$this->command_name = $command->getName();
+
+		return new CommandTester($command);
+	}
+}

From 16f9b4630cfc3c6247894ac82ac6b95577075753 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sat, 26 Mar 2016 12:38:07 -0700
Subject: [PATCH 03/12] [ticket/14561] Reclean usernames command

PHPBB3-14561
---
 .../default/container/services_console.yml    |   9 ++
 phpBB/language/en/cli.php                     |   6 +
 phpBB/phpbb/console/command/user/reclean.php  | 125 ++++++++++++++++++
 tests/console/user/reclean_test.php           |  78 +++++++++++
 4 files changed, 218 insertions(+)
 create mode 100644 phpBB/phpbb/console/command/user/reclean.php
 create mode 100644 tests/console/user/reclean_test.php

diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml
index 710487dfe8..994ac55ee9 100644
--- a/phpBB/config/default/container/services_console.yml
+++ b/phpBB/config/default/container/services_console.yml
@@ -258,3 +258,12 @@ services:
             - '%core.php_ext%'
         tags:
             - { name: console.command }
+
+    console.command.user.reclean:
+        class: phpbb\console\command\user\reclean
+        arguments:
+            - '@user'
+            - '@dbal.conn'
+            - '@language'
+        tags:
+            - { name: console.command }
diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php
index d76993e3bf..0048e4569f 100644
--- a/phpBB/language/en/cli.php
+++ b/phpBB/language/en/cli.php
@@ -95,6 +95,7 @@ $lang = array_merge($lang, array(
 	'CLI_DESCRIPTION_USER_DELETE'				=> 'Delete a user account.',
 	'CLI_DESCRIPTION_USER_DELETE_USERNAME'		=> 'Username of the user to delete',
 	'CLI_DESCRIPTION_USER_DELETE_OPTION_POSTS'	=> 'Delete all posts by the user. Without this option, the user’s posts will be retained.',
+	'CLI_DESCRIPTION_USER_RECLEAN'				=> 'Re-clean usernames.',
 
 	'CLI_EXTENSION_DISABLE_FAILURE'		=> 'Could not disable extension %s',
 	'CLI_EXTENSION_DISABLE_SUCCESS'		=> 'Successfully disabled extension %s',
@@ -135,6 +136,11 @@ $lang = array_merge($lang, array(
 
 	'CLI_USER_ADD_SUCCESS'		=> 'Successfully added user %s.',
 	'CLI_USER_DELETE_CONFIRM'	=> 'Are you sure you want to delete ‘%s’? [y/N]',
+	'CLI_USER_RECLEAN_SUCCESS'	=> [
+		0	=> 'Re-cleaning complete. No usernames needed to be cleaned.',
+		1	=> 'Re-cleaning complete. %d username was cleaned.',
+		2	=> 'Re-cleaning complete. %d usernames were cleaned.',
+	],
 ));
 
 // Additional help for commands.
diff --git a/phpBB/phpbb/console/command/user/reclean.php b/phpBB/phpbb/console/command/user/reclean.php
new file mode 100644
index 0000000000..c53d766cce
--- /dev/null
+++ b/phpBB/phpbb/console/command/user/reclean.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\console\command\user;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+class reclean extends \phpbb\console\command\command
+{
+	/** @var \phpbb\db\driver\driver_interface */
+	protected $db;
+
+	/** @var \phpbb\language\language */
+	protected $language;
+
+	/** @var int A count of the number of re-cleaned user names */
+	protected $processed;
+
+	/**
+	 * Construct method
+	 *
+	 * @param \phpbb\user                       $user
+	 * @param \phpbb\db\driver\driver_interface $db
+	 * @param \phpbb\language\language          $language
+	 */
+	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\language\language $language)
+	{
+		$this->db = $db;
+		$this->language = $language;
+
+		parent::__construct($user);
+	}
+
+	/**
+	 * Sets the command name and description
+	 *
+	 * @return null
+	 */
+	protected function configure()
+	{
+		$this
+			->setName('user:reclean')
+			->setDescription($this->language->lang('CLI_DESCRIPTION_USER_RECLEAN'))
+		;
+	}
+
+	/**
+	 * Executes the command user:reclean
+	 *
+	 * Cleans user names that are unclean.
+	 *
+	 * @param InputInterface  $input  The input stream used to get the options
+	 * @param OutputInterface $output The output stream, used to print messages
+	 *
+	 * @return int 0 if all is well, 1 if any errors occurred
+	 */
+	protected function execute(InputInterface $input, OutputInterface $output)
+	{
+		$this->processed = 0;
+
+		$stage = 0;
+		while ($stage !== true)
+		{
+			$stage = $this->reclean_usernames($stage);
+		}
+
+		$io = new SymfonyStyle($input, $output);
+		$io->success($this->language->lang('CLI_USER_RECLEAN_SUCCESS', $this->processed));
+		return 0;
+	}
+
+	/**
+	 * Re-clean user names
+	 * Only user names that are unclean will be re-cleaned
+	 *
+	 * @param int $start An offset index
+	 * @return bool|int Return the next offset index or true if all records have been processed.
+	 */
+	protected function reclean_usernames($start = 0)
+	{
+		$limit = 500;
+		$i = 0;
+
+		$this->db->sql_transaction('begin');
+
+		$sql = 'SELECT user_id, username, username_clean FROM ' . USERS_TABLE;
+		$result = $this->db->sql_query_limit($sql, $limit, $start);
+		while ($row = $this->db->sql_fetchrow($result))
+		{
+			$i++;
+			$username_clean = $this->db->sql_escape(utf8_clean_string($row['username']));
+
+			if ($username_clean != $row['username_clean'])
+			{
+				$sql = 'UPDATE ' . USERS_TABLE . "
+					SET username_clean = '$username_clean'
+					WHERE user_id = {$row['user_id']}";
+				$this->db->sql_query($sql);
+
+				$this->processed++;
+			}
+		}
+		$this->db->sql_freeresult($result);
+
+		$this->db->sql_transaction('commit');
+
+		return ($i < $limit) ? true : $start + $i;
+	}
+}
diff --git a/tests/console/user/reclean_test.php b/tests/console/user/reclean_test.php
new file mode 100644
index 0000000000..ac464bdf82
--- /dev/null
+++ b/tests/console/user/reclean_test.php
@@ -0,0 +1,78 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\CommandTester;
+use phpbb\console\command\user\reclean;
+
+class phpbb_console_command_user_reclean_test extends phpbb_database_test_case
+{
+	protected $db;
+	protected $user;
+	protected $language;
+	protected $command_name;
+
+	public function getDataSet()
+	{
+		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
+	}
+
+	public function setUp()
+	{
+		$this->db = $this->new_dbal();
+
+		$this->language = $this->getMockBuilder('\phpbb\language\language')
+			->disableOriginalConstructor()
+			->getMock();
+		$this->language->expects($this->any())
+			->method('lang')
+			->will($this->returnArgument(0));
+		$this->user = $this->getMock('\phpbb\user', array(), array(
+			$this->language,
+			'\phpbb\datetime'
+		));
+
+		parent::setUp();
+	}
+
+	public function test_reclean()
+	{
+		$command_tester = $this->get_command_tester();
+
+		$command_tester->execute(array(
+			'command' => $this->command_name,
+		));
+
+		$this->assertContains('CLI_USER_RECLEAN_SUCCESS', $command_tester->getDisplay());
+
+		$result = $this->db->sql_query('SELECT user_id FROM ' . USERS_TABLE . " WHERE username_clean = 'test unclean'");
+		$row = $this->db->sql_fetchrow($result);
+		$this->db->sql_freeresult($result);
+		$this->assertNotNull($row['user_id']);
+	}
+
+	public function get_command_tester()
+	{
+		$application = new Application();
+		$application->add(new reclean(
+			$this->user,
+			$this->db,
+			$this->language
+		));
+
+		$command = $application->find('user:reclean');
+		$this->command_name = $command->getName();
+
+		return new CommandTester($command);
+	}
+}

From 6291bfaca94d104f1c85a0b139cab908e0793b3a Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sat, 26 Mar 2016 12:59:23 -0700
Subject: [PATCH 04/12] [ticket/14561] Refactor tests

PHPBB3-14561
---
 tests/console/user/activate_test.php   |  85 +++++------------
 tests/console/user/add_test.php        | 121 +++---------------------
 tests/console/user/base.php            | 122 +++++++++++++++++++++++++
 tests/console/user/delete_test.php     | 107 +++-------------------
 tests/console/user/fixtures/config.xml |  20 ++++
 tests/console/user/reclean_test.php    |  52 +++--------
 6 files changed, 208 insertions(+), 299 deletions(-)
 create mode 100644 tests/console/user/base.php

diff --git a/tests/console/user/activate_test.php b/tests/console/user/activate_test.php
index 90a4d74a26..08b25c6c95 100644
--- a/tests/console/user/activate_test.php
+++ b/tests/console/user/activate_test.php
@@ -15,58 +15,39 @@ use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Tester\CommandTester;
 use phpbb\console\command\user\activate;
 
-class phpbb_console_command_user_activate_test extends phpbb_database_test_case
-{
-	protected $db;
-	protected $config;
-	protected $user;
-	protected $language;
-	protected $log;
-	protected $notifications;
-	protected $command_name;
-	protected $phpbb_root_path;
-	protected $php_ext;
+require_once dirname(__FILE__) . '/base.php';
 
-	public function getDataSet()
-	{
-		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
-	}
+class phpbb_console_user_activate_test extends phpbb_console_user_base
+{
+	protected $notifications;
 
 	public function setUp()
 	{
-		global $config, $db, $user, $auth, $phpbb_dispatcher, $phpbb_root_path, $phpEx;
-
-		$auth = $this->getMock('\phpbb\auth\auth');
-
-		$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
-
-		$config = $this->config = new \phpbb\config\config(array());
-
-		$db = $this->db = $this->new_dbal();
-
-		$this->language = $this->getMockBuilder('\phpbb\language\language')
-			->disableOriginalConstructor()
-			->getMock();
-		$this->language->expects($this->any())
-			->method('lang')
-			->will($this->returnArgument(0));
-		$user = $this->user = $this->getMock('\phpbb\user', array(), array(
-			$this->language,
-			'\phpbb\datetime'
-		));
-
-		$this->log = $this->getMockBuilder('\phpbb\log\log')
-			->disableOriginalConstructor()
-			->getMock();
+		parent::setUp();
 
 		$this->notifications = $this->getMockBuilder('\phpbb\notification\manager')
 			->disableOriginalConstructor()
 			->getMock();
+	}
 
-		$this->phpbb_root_path = $phpbb_root_path;
-		$this->php_ext = $phpEx;
+	public function get_command_tester()
+	{
+		$application = new Application();
+		$application->add(new activate(
+			$this->user,
+			$this->db,
+			$this->config,
+			$this->language,
+			$this->log,
+			$this->notifications,
+			$this->phpbb_root_path,
+			$this->php_ext
+		));
 
-		parent::setUp();
+		$command = $application->find('user:activate');
+		$this->command_name = $command->getName();
+
+		return new CommandTester($command);
 	}
 
 	public function activate_test_data()
@@ -101,24 +82,4 @@ class phpbb_console_command_user_activate_test extends phpbb_database_test_case
 
 		$this->assertContains($expected, $command_tester->getDisplay());
 	}
-
-	public function get_command_tester()
-	{
-		$application = new Application();
-		$application->add(new activate(
-			$this->user,
-			$this->db,
-			$this->config,
-			$this->language,
-			$this->log,
-			$this->notifications,
-			$this->phpbb_root_path,
-			$this->php_ext
-		));
-
-		$command = $application->find('user:activate');
-		$this->command_name = $command->getName();
-
-		return new CommandTester($command);
-	}
 }
diff --git a/tests/console/user/add_test.php b/tests/console/user/add_test.php
index ee6eee8491..8641bf87b6 100644
--- a/tests/console/user/add_test.php
+++ b/tests/console/user/add_test.php
@@ -15,75 +15,27 @@ use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Tester\CommandTester;
 use phpbb\console\command\user\add;
 
-require_once dirname(__FILE__) . '/../../../phpBB/includes/functions_user.php';
+require_once dirname(__FILE__) . '/base.php';
 
-class phpbb_console_command_user_add_test extends phpbb_database_test_case
+class phpbb_console_user_add_test extends phpbb_console_user_base
 {
-	protected $db;
-	protected $config;
-	protected $user;
-	protected $language;
-	protected $passwords_manager;
-	protected $command_name;
-	protected $question;
-	protected $phpbb_root_path;
-	protected $php_ext;
-
-	public function getDataSet()
+	public function get_command_tester()
 	{
-		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
-	}
-
-	public function setUp()
-	{
-		global $db, $cache, $config, $user, $phpbb_dispatcher, $phpbb_container, $phpbb_root_path, $phpEx;
-
-		$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
-		$phpbb_container = new phpbb_mock_container_builder();
-		$phpbb_container->set('cache.driver', new phpbb_mock_cache());
-		$phpbb_container->set('notification_manager', new phpbb_mock_notification_manager());
-
-		$cache = $phpbb_container->get('cache.driver');
-
-		$config = $this->config = new \phpbb\config\config(array(
-			'board_timezone'	=> 'UTC',
-			'default_lang'		=> 'en',
-			'email_enable'		=> false,
-			'min_name_chars'	=> 3,
-			'max_name_chars'	=> 10,
-			'min_pass_chars'	=> 3,
-			'max_pass_chars'	=> 10,
-			'pass_complex'		=> 'PASS_TYPE_ANY',
-		));
-
-		$db = $this->db = $this->new_dbal();
-
-		$this->language = $this->getMockBuilder('\phpbb\language\language')
-			->disableOriginalConstructor()
-			->getMock();
-		$this->language->expects($this->any())
-			->method('lang')
-			->will($this->returnArgument(0));
-		$user = $this->user = $this->getMock('\phpbb\user', array(), array(
+		$application = new Application();
+		$application->add(new add(
+			$this->user,
+			$this->db,
+			$this->config,
 			$this->language,
-			'\phpbb\datetime'
+			$this->passwords_manager,
+			$this->phpbb_root_path,
+			$this->php_ext
 		));
 
-		$driver_helper = new \phpbb\passwords\driver\helper($this->config);
-		$passwords_drivers = array(
-			'passwords.driver.bcrypt_2y'	=> new \phpbb\passwords\driver\bcrypt_2y($this->config, $driver_helper),
-			'passwords.driver.bcrypt'		=> new \phpbb\passwords\driver\bcrypt($this->config, $driver_helper),
-			'passwords.driver.salted_md5'	=> new \phpbb\passwords\driver\salted_md5($this->config, $driver_helper),
-			'passwords.driver.phpass'		=> new \phpbb\passwords\driver\phpass($this->config, $driver_helper),
-		);
-
-		$passwords_helper = new \phpbb\passwords\helper;
-		$this->passwords_manager = new \phpbb\passwords\manager($this->config, $passwords_drivers, $passwords_helper, array_keys($passwords_drivers));
-
-		$this->phpbb_root_path = $phpbb_root_path;
-		$this->php_ext = $phpEx;
-
-		parent::setUp();
+		$command = $application->find('user:add');
+		$this->command_name = $command->getName();
+		$this->question = $command->getHelper('question');
+		return new CommandTester($command);
 	}
 
 	public function test_add_no_dialog()
@@ -137,47 +89,4 @@ class phpbb_console_command_user_add_test extends phpbb_database_test_case
 		$this->assertContains('TOO_SHORT', $command_tester->getDisplay());
 		$this->assertContains('EMAIL_INVALID', $command_tester->getDisplay());
 	}
-
-	public function get_command_tester()
-	{
-		$application = new Application();
-		$application->add(new add(
-			$this->user,
-			$this->db,
-			$this->config,
-			$this->language,
-			$this->passwords_manager,
-			$this->phpbb_root_path,
-			$this->php_ext
-		));
-
-		$command = $application->find('user:add');
-		$this->command_name = $command->getName();
-		$this->question = $command->getHelper('question');
-		return new CommandTester($command);
-	}
-
-	public function get_user_id($username)
-	{
-		$sql = 'SELECT user_id
-			FROM ' . USERS_TABLE . '
-			WHERE ' . 'username = ' . "'" . $username . "'";
-
-		$result = $this->db->sql_query($sql);
-
-		$row = $this->db->sql_fetchrow($result);
-
-		$this->db->sql_freeresult($result);
-
-		return $row['user_id'];
-	}
-
-	public function getInputStream($input)
-	{
-		$stream = fopen('php://memory', 'r+', false);
-		fputs($stream, $input);
-		rewind($stream);
-
-		return $stream;
-	}
 }
diff --git a/tests/console/user/base.php b/tests/console/user/base.php
new file mode 100644
index 0000000000..c6ffc428ed
--- /dev/null
+++ b/tests/console/user/base.php
@@ -0,0 +1,122 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+require_once dirname(__FILE__) . '/../../../phpBB/includes/functions_user.php';
+require_once dirname(__FILE__) . '/../../../phpBB/includes/functions.php';
+require_once dirname(__FILE__) . '/../../../phpBB/includes/utf/utf_tools.php';
+
+abstract class phpbb_console_user_base extends phpbb_database_test_case
+{
+	protected $db;
+	protected $config;
+	protected $user;
+	protected $language;
+	protected $log;
+	protected $passwords_manager;
+	protected $command_name;
+	protected $question;
+	protected $phpbb_root_path;
+	protected $php_ext;
+
+	public function getDataSet()
+	{
+		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
+	}
+
+	public function setUp()
+	{
+		global $auth, $db, $cache, $config, $user, $phpbb_dispatcher, $phpbb_container, $phpbb_root_path, $phpEx;
+
+		$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
+		$phpbb_container = new phpbb_mock_container_builder();
+		$phpbb_container->set('cache.driver', new phpbb_mock_cache());
+		$phpbb_container->set('notification_manager', new phpbb_mock_notification_manager());
+
+		$auth = $this->getMock('\phpbb\auth\auth');
+
+		$cache = $phpbb_container->get('cache.driver');
+
+		$config = $this->config = new \phpbb\config\config(array(
+			'board_timezone'	=> 'UTC',
+			'default_lang'		=> 'en',
+			'email_enable'		=> false,
+			'min_name_chars'	=> 3,
+			'max_name_chars'	=> 10,
+			'min_pass_chars'	=> 3,
+			'max_pass_chars'	=> 10,
+			'pass_complex'		=> 'PASS_TYPE_ANY',
+		));
+
+		$db = $this->db = $this->new_dbal();
+
+		$this->language = $this->getMockBuilder('\phpbb\language\language')
+			->disableOriginalConstructor()
+			->getMock();
+		$this->language->expects($this->any())
+			->method('lang')
+			->will($this->returnArgument(0));
+		$user = $this->user = $this->getMock('\phpbb\user', array(), array(
+			$this->language,
+			'\phpbb\datetime'
+		));
+
+		$driver_helper = new \phpbb\passwords\driver\helper($this->config);
+		$passwords_drivers = array(
+			'passwords.driver.bcrypt_2y'	=> new \phpbb\passwords\driver\bcrypt_2y($this->config, $driver_helper),
+			'passwords.driver.bcrypt'		=> new \phpbb\passwords\driver\bcrypt($this->config, $driver_helper),
+			'passwords.driver.salted_md5'	=> new \phpbb\passwords\driver\salted_md5($this->config, $driver_helper),
+			'passwords.driver.phpass'		=> new \phpbb\passwords\driver\phpass($this->config, $driver_helper),
+		);
+
+		$passwords_helper = new \phpbb\passwords\helper;
+		$this->passwords_manager = new \phpbb\passwords\manager($this->config, $passwords_drivers, $passwords_helper, array_keys($passwords_drivers));
+
+		$this->phpbb_root_path = $phpbb_root_path;
+		$this->php_ext = $phpEx;
+
+		$this->log = $this->getMockBuilder('\phpbb\log\log')
+			->disableOriginalConstructor()
+			->getMock();
+
+		$phpbb_container->set('auth.provider.db', new phpbb_mock_auth_provider());
+		$provider_collection = new \phpbb\auth\provider_collection($phpbb_container, $config);
+		$provider_collection->add('auth.provider.db');
+		$phpbb_container->set(
+			'auth.provider_collection',
+			$provider_collection
+		);
+
+		parent::setUp();
+	}
+
+	public function get_user_id($username)
+	{
+		$sql = 'SELECT user_id
+			FROM ' . USERS_TABLE . '
+			WHERE ' . 'username = ' . "'" . $username . "'";
+		$result = $this->db->sql_query($sql);
+		$row = $this->db->sql_fetchrow($result);
+		$this->db->sql_freeresult($result);
+
+		return $row['user_id'];
+	}
+
+	public function getInputStream($input)
+	{
+		$stream = fopen('php://memory', 'r+', false);
+		fputs($stream, $input);
+		rewind($stream);
+
+		return $stream;
+	}
+}
diff --git a/tests/console/user/delete_test.php b/tests/console/user/delete_test.php
index 5162358713..dc4c07e8b8 100644
--- a/tests/console/user/delete_test.php
+++ b/tests/console/user/delete_test.php
@@ -15,64 +15,27 @@ use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Tester\CommandTester;
 use phpbb\console\command\user\delete;
 
-class phpbb_console_command_user_delete_test extends phpbb_database_test_case
+require_once dirname(__FILE__) . '/base.php';
+
+class phpbb_console_user_delete_test extends phpbb_console_user_base
 {
-	protected $db;
-	protected $user;
-	protected $language;
-	protected $log;
-	protected $command_name;
-	protected $question;
-	protected $phpbb_root_path;
-	protected $php_ext;
-
-	public function getDataSet()
+	public function get_command_tester()
 	{
-		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
-	}
-
-	public function setUp()
-	{
-		global $db, $cache, $config, $user, $phpbb_dispatcher, $phpbb_container, $phpbb_root_path, $phpEx;
-
-		$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
-		$phpbb_container = new phpbb_mock_container_builder();
-		$phpbb_container->set('cache.driver', new phpbb_mock_cache());
-		$phpbb_container->set('notification_manager', new phpbb_mock_notification_manager());
-
-		$cache = $phpbb_container->get('cache.driver');
-
-		$config = new \phpbb\config\config(array());
-
-		$db = $this->db = $this->new_dbal();
-
-		$this->language = $this->getMockBuilder('\phpbb\language\language')
-			->disableOriginalConstructor()
-			->getMock();
-		$this->language->expects($this->any())
-			->method('lang')
-			->will($this->returnArgument(0));
-		$user = $this->user = $this->getMock('\phpbb\user', array(), array(
+		$application = new Application();
+		$application->add(new delete(
+			$this->user,
+			$this->db,
 			$this->language,
-			'\phpbb\datetime'
+			$this->log,
+			$this->phpbb_root_path,
+			$this->php_ext
 		));
 
-		$this->log = $this->getMockBuilder('\phpbb\log\log')
-			->disableOriginalConstructor()
-			->getMock();
+		$command = $application->find('user:delete');
+		$this->command_name = $command->getName();
+		$this->question = $command->getHelper('question');
 
-		$phpbb_container->set('auth.provider.db', new phpbb_mock_auth_provider());
-		$provider_collection = new \phpbb\auth\provider_collection($phpbb_container, $config);
-		$provider_collection->add('auth.provider.db');
-		$phpbb_container->set(
-			'auth.provider_collection',
-			$provider_collection
-		);
-
-		$this->phpbb_root_path = $phpbb_root_path;
-		$this->php_ext = $phpEx;
-
-		parent::setUp();
+		return new CommandTester($command);
 	}
 
 	public function test_delete()
@@ -126,44 +89,4 @@ class phpbb_console_command_user_delete_test extends phpbb_database_test_case
 
 		$this->assertNotNull($this->get_user_id('Test'));
 	}
-
-	public function get_command_tester()
-	{
-		$application = new Application();
-		$application->add(new delete(
-			$this->user,
-			$this->db,
-			$this->language,
-			$this->log,
-			$this->phpbb_root_path,
-			$this->php_ext
-		));
-
-		$command = $application->find('user:delete');
-		$this->command_name = $command->getName();
-		$this->question = $command->getHelper('question');
-
-		return new CommandTester($command);
-	}
-
-	public function get_user_id($username)
-	{
-		$sql = 'SELECT user_id
-			FROM ' . USERS_TABLE . '
-			WHERE ' . 'username = ' . "'" . $username . "'";
-		$result = $this->db->sql_query($sql);
-		$row = $this->db->sql_fetchrow($result);
-		$this->db->sql_freeresult($result);
-
-		return $row['user_id'];
-	}
-
-	public function getInputStream($input)
-	{
-		$stream = fopen('php://memory', 'r+', false);
-		fputs($stream, $input);
-		rewind($stream);
-
-		return $stream;
-	}
 }
diff --git a/tests/console/user/fixtures/config.xml b/tests/console/user/fixtures/config.xml
index fed30dc20d..a988ba463f 100644
--- a/tests/console/user/fixtures/config.xml
+++ b/tests/console/user/fixtures/config.xml
@@ -6,12 +6,14 @@
 		<column>username</column>
 		<column>username_clean</column>
 		<column>user_sig</column>
+		<column>user_type</column>
 		<row>
 			<value>1</value>
 			<value></value>
 			<value>Guest</value>
 			<value>guest</value>
 			<value></value>
+			<value>0</value>
 		</row>
 		<row>
 			<value>2</value>
@@ -19,6 +21,7 @@
 			<value>Admin</value>
 			<value>admin</value>
 			<value></value>
+			<value>3</value>
 		</row>
 		<row>
 			<value>3</value>
@@ -26,6 +29,23 @@
 			<value>Test</value>
 			<value>test</value>
 			<value></value>
+			<value>1</value>
+		</row>
+		<row>
+			<value>4</value>
+			<value></value>
+			<value>Test 2</value>
+			<value>test 2</value>
+			<value></value>
+			<value>0</value>
+		</row>
+		<row>
+			<value>5</value>
+			<value></value>
+			<value>Test Unclean</value>
+			<value>Test Unclean</value>
+			<value></value>
+			<value>0</value>
 		</row>
 	</table>
 	<table name="phpbb_groups">
diff --git a/tests/console/user/reclean_test.php b/tests/console/user/reclean_test.php
index ac464bdf82..2b28484084 100644
--- a/tests/console/user/reclean_test.php
+++ b/tests/console/user/reclean_test.php
@@ -15,34 +15,23 @@ use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Tester\CommandTester;
 use phpbb\console\command\user\reclean;
 
-class phpbb_console_command_user_reclean_test extends phpbb_database_test_case
+require_once dirname(__FILE__) . '/base.php';
+
+class phpbb_console_user_reclean_test extends phpbb_console_user_base
 {
-	protected $db;
-	protected $user;
-	protected $language;
-	protected $command_name;
-
-	public function getDataSet()
+	public function get_command_tester()
 	{
-		return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/config.xml');
-	}
-
-	public function setUp()
-	{
-		$this->db = $this->new_dbal();
-
-		$this->language = $this->getMockBuilder('\phpbb\language\language')
-			->disableOriginalConstructor()
-			->getMock();
-		$this->language->expects($this->any())
-			->method('lang')
-			->will($this->returnArgument(0));
-		$this->user = $this->getMock('\phpbb\user', array(), array(
-			$this->language,
-			'\phpbb\datetime'
+		$application = new Application();
+		$application->add(new reclean(
+			$this->user,
+			$this->db,
+			$this->language
 		));
 
-		parent::setUp();
+		$command = $application->find('user:reclean');
+		$this->command_name = $command->getName();
+
+		return new CommandTester($command);
 	}
 
 	public function test_reclean()
@@ -60,19 +49,4 @@ class phpbb_console_command_user_reclean_test extends phpbb_database_test_case
 		$this->db->sql_freeresult($result);
 		$this->assertNotNull($row['user_id']);
 	}
-
-	public function get_command_tester()
-	{
-		$application = new Application();
-		$application->add(new reclean(
-			$this->user,
-			$this->db,
-			$this->language
-		));
-
-		$command = $application->find('user:reclean');
-		$this->command_name = $command->getName();
-
-		return new CommandTester($command);
-	}
 }

From 13f365916caf9b01312da3717359316faa576521 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sat, 26 Mar 2016 14:50:45 -0700
Subject: [PATCH 05/12] [ticket/14561] Remove unused use statements

PHPBB3-14561
---
 phpBB/phpbb/console/command/user/activate.php | 2 --
 phpBB/phpbb/console/command/user/delete.php   | 1 -
 phpBB/phpbb/console/command/user/reclean.php  | 4 ----
 3 files changed, 7 deletions(-)

diff --git a/phpBB/phpbb/console/command/user/activate.php b/phpBB/phpbb/console/command/user/activate.php
index 890827afb6..697c1e5abe 100644
--- a/phpBB/phpbb/console/command/user/activate.php
+++ b/phpBB/phpbb/console/command/user/activate.php
@@ -17,8 +17,6 @@ use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ConfirmationQuestion;
-use Symfony\Component\Console\Question\Question;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
 class activate extends \phpbb\console\command\command
diff --git a/phpBB/phpbb/console/command/user/delete.php b/phpBB/phpbb/console/command/user/delete.php
index 360b119e17..7251ecb3a5 100644
--- a/phpBB/phpbb/console/command/user/delete.php
+++ b/phpBB/phpbb/console/command/user/delete.php
@@ -18,7 +18,6 @@ use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Question\ConfirmationQuestion;
-use Symfony\Component\Console\Question\Question;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
 class delete extends \phpbb\console\command\command
diff --git a/phpBB/phpbb/console/command/user/reclean.php b/phpBB/phpbb/console/command/user/reclean.php
index c53d766cce..cd5fc60a05 100644
--- a/phpBB/phpbb/console/command/user/reclean.php
+++ b/phpBB/phpbb/console/command/user/reclean.php
@@ -13,12 +13,8 @@
 
 namespace phpbb\console\command\user;
 
-use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ConfirmationQuestion;
-use Symfony\Component\Console\Question\Question;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
 class reclean extends \phpbb\console\command\command

From aee3eec439b39ac1f8aa79582b302a499a23acc0 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sun, 27 Mar 2016 09:31:47 -0700
Subject: [PATCH 06/12] [ticket/14561] Import classes with use statements

PHPBB3-14561
---
 phpBB/phpbb/console/command/user/activate.php | 37 +++++++++++--------
 phpBB/phpbb/console/command/user/add.php      | 32 +++++++++-------
 phpBB/phpbb/console/command/user/delete.php   | 27 ++++++++------
 phpBB/phpbb/console/command/user/reclean.php  | 18 +++++----
 4 files changed, 68 insertions(+), 46 deletions(-)

diff --git a/phpBB/phpbb/console/command/user/activate.php b/phpBB/phpbb/console/command/user/activate.php
index 697c1e5abe..9eab06847c 100644
--- a/phpBB/phpbb/console/command/user/activate.php
+++ b/phpBB/phpbb/console/command/user/activate.php
@@ -13,27 +13,34 @@
 
 namespace phpbb\console\command\user;
 
+use phpbb\config\config;
+use phpbb\console\command\command;
+use phpbb\db\driver\driver_interface;
+use phpbb\language\language;
+use phpbb\log\log_interface;
+use phpbb\notification\manager;
+use phpbb\user;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
-class activate extends \phpbb\console\command\command
+class activate extends command
 {
-	/** @var \phpbb\db\driver\driver_interface */
+	/** @var driver_interface */
 	protected $db;
 
-	/** @var \phpbb\config\config */
+	/** @var config */
 	protected $config;
 
-	/** @var \phpbb\language\language */
+	/** @var language */
 	protected $language;
 
-	/** @var \phpbb\log\log_interface */
+	/** @var log_interface */
 	protected $log;
 
-	/** @var \phpbb\notification\manager */
+	/** @var manager */
 	protected $notifications;
 
 	/**
@@ -53,16 +60,16 @@ class activate extends \phpbb\console\command\command
 	/**
 	 * Construct method
 	 *
-	 * @param \phpbb\user                       $user
-	 * @param \phpbb\db\driver\driver_interface $db
-	 * @param \phpbb\config\config              $config
-	 * @param \phpbb\language\language          $language
-	 * @param \phpbb\log\log_interface          $log
-	 * @param \phpbb\notification\manager       $notifications
-	 * @param string                            $phpbb_root_path
-	 * @param string                            $php_ext
+	 * @param user             $user
+	 * @param driver_interface $db
+	 * @param config           $config
+	 * @param language         $language
+	 * @param log_interface    $log
+	 * @param manager          $notifications
+	 * @param string           $phpbb_root_path
+	 * @param string           $php_ext
 	 */
-	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\language\language $language, \phpbb\log\log_interface $log, \phpbb\notification\manager $notifications, $phpbb_root_path, $php_ext)
+	public function __construct(user $user, driver_interface $db, config $config, language $language, log_interface $log, manager $notifications, $phpbb_root_path, $php_ext)
 	{
 		$this->db = $db;
 		$this->config = $config;
diff --git a/phpBB/phpbb/console/command/user/add.php b/phpBB/phpbb/console/command/user/add.php
index df1f4aa54a..c60a059251 100644
--- a/phpBB/phpbb/console/command/user/add.php
+++ b/phpBB/phpbb/console/command/user/add.php
@@ -13,28 +13,34 @@
 
 namespace phpbb\console\command\user;
 
+use phpbb\config\config;
+use phpbb\console\command\command;
+use phpbb\db\driver\driver_interface;
 use phpbb\exception\runtime_exception;
+use phpbb\language\language;
+use phpbb\passwords\manager;
+use phpbb\user;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Question\Question;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
-class add extends \phpbb\console\command\command
+class add extends command
 {
 	/** @var array Array of interactively acquired options */
 	protected $data;
 
-	/** @var \phpbb\db\driver\driver_interface */
+	/** @var driver_interface */
 	protected $db;
 
-	/** @var \phpbb\config\config */
+	/** @var config */
 	protected $config;
 
-	/** @var \phpbb\language\language */
+	/** @var language */
 	protected $language;
 
-	/** @var \phpbb\passwords\manager */
+	/** @var manager */
 	protected $password_manager;
 
 	/**
@@ -54,15 +60,15 @@ class add extends \phpbb\console\command\command
 	/**
 	 * Construct method
 	 *
-	 * @param \phpbb\user                       $user
-	 * @param \phpbb\db\driver\driver_interface $db
-	 * @param \phpbb\config\config              $config
-	 * @param \phpbb\language\language          $language
-	 * @param \phpbb\passwords\manager          $password_manager
-	 * @param string                            $phpbb_root_path
-	 * @param string                            $php_ext
+	 * @param user             $user
+	 * @param driver_interface $db
+	 * @param config           $config
+	 * @param language         $language
+	 * @param manager          $password_manager
+	 * @param string           $phpbb_root_path
+	 * @param string           $php_ext
 	 */
-	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\language\language $language, \phpbb\passwords\manager $password_manager, $phpbb_root_path, $php_ext)
+	public function __construct(user $user, driver_interface $db, config $config, language $language, manager $password_manager, $phpbb_root_path, $php_ext)
 	{
 		$this->db = $db;
 		$this->config = $config;
diff --git a/phpBB/phpbb/console/command/user/delete.php b/phpBB/phpbb/console/command/user/delete.php
index 7251ecb3a5..93e75d365b 100644
--- a/phpBB/phpbb/console/command/user/delete.php
+++ b/phpBB/phpbb/console/command/user/delete.php
@@ -13,6 +13,11 @@
 
 namespace phpbb\console\command\user;
 
+use phpbb\console\command\command;
+use phpbb\db\driver\driver_interface;
+use phpbb\language\language;
+use phpbb\log\log_interface;
+use phpbb\user;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -20,15 +25,15 @@ use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Question\ConfirmationQuestion;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
-class delete extends \phpbb\console\command\command
+class delete extends command
 {
-	/** @var \phpbb\db\driver\driver_interface */
+	/** @var driver_interface */
 	protected $db;
 
-	/** @var \phpbb\language\language */
+	/** @var language */
 	protected $language;
 
-	/** @var \phpbb\log\log_interface */
+	/** @var log_interface */
 	protected $log;
 
 	/**
@@ -48,14 +53,14 @@ class delete extends \phpbb\console\command\command
 	/**
 	 * Construct method
 	 *
-	 * @param \phpbb\user                       $user
-	 * @param \phpbb\db\driver\driver_interface $db
-	 * @param \phpbb\language\language          $language
-	 * @param \phpbb\log\log_interface          $log
-	 * @param string                            $phpbb_root_path
-	 * @param string                            $php_ext
+	 * @param user             $user
+	 * @param driver_interface $db
+	 * @param language         $language
+	 * @param log_interface    $log
+	 * @param string           $phpbb_root_path
+	 * @param string           $php_ext
 	 */
-	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\language\language $language, \phpbb\log\log_interface $log, $phpbb_root_path, $php_ext)
+	public function __construct(user $user, driver_interface $db, language $language, log_interface $log, $phpbb_root_path, $php_ext)
 	{
 		$this->db = $db;
 		$this->language = $language;
diff --git a/phpBB/phpbb/console/command/user/reclean.php b/phpBB/phpbb/console/command/user/reclean.php
index cd5fc60a05..e2f95c16d8 100644
--- a/phpBB/phpbb/console/command/user/reclean.php
+++ b/phpBB/phpbb/console/command/user/reclean.php
@@ -13,16 +13,20 @@
 
 namespace phpbb\console\command\user;
 
+use phpbb\console\command\command;
+use phpbb\db\driver\driver_interface;
+use phpbb\language\language;
+use phpbb\user;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
 
-class reclean extends \phpbb\console\command\command
+class reclean extends command
 {
-	/** @var \phpbb\db\driver\driver_interface */
+	/** @var driver_interface */
 	protected $db;
 
-	/** @var \phpbb\language\language */
+	/** @var language */
 	protected $language;
 
 	/** @var int A count of the number of re-cleaned user names */
@@ -31,11 +35,11 @@ class reclean extends \phpbb\console\command\command
 	/**
 	 * Construct method
 	 *
-	 * @param \phpbb\user                       $user
-	 * @param \phpbb\db\driver\driver_interface $db
-	 * @param \phpbb\language\language          $language
+	 * @param user             $user
+	 * @param driver_interface $db
+	 * @param language         $language
 	 */
-	public function __construct(\phpbb\user $user, \phpbb\db\driver\driver_interface $db, \phpbb\language\language $language)
+	public function __construct(user $user, driver_interface $db, language $language)
 	{
 		$this->db = $db;
 		$this->language = $language;

From ed0f151d863d7449d73336b3697e37259812215e Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sun, 27 Mar 2016 09:47:45 -0700
Subject: [PATCH 07/12] [ticket/14561] Add extra help explaining reclean
 command

PHPBB3-14561
---
 phpBB/language/en/cli.php                    | 1 +
 phpBB/phpbb/console/command/user/reclean.php | 1 +
 2 files changed, 2 insertions(+)

diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php
index 0048e4569f..872aa464b5 100644
--- a/phpBB/language/en/cli.php
+++ b/phpBB/language/en/cli.php
@@ -151,4 +151,5 @@ To optionally send an activation email to the user, use the <info>--send-email</
 	'CLI_HELP_USER_ADD'			=> 'The <info>%command.name%</info> command adds a new user:
 If this command is run without options, you will be prompted to enter them.
 To optionally send an email to the new user, use the <info>--send-email</info> option.',
+	'CLI_HELP_USER_RECLEAN'		=> 'Re-clean usernames will check all stored usernames and ensure clean versions are also stored. Cleaned usernames are a case insensitive form, NFC normalized and transformed to ASCII.',
 ));
diff --git a/phpBB/phpbb/console/command/user/reclean.php b/phpBB/phpbb/console/command/user/reclean.php
index e2f95c16d8..ba8a638e7b 100644
--- a/phpBB/phpbb/console/command/user/reclean.php
+++ b/phpBB/phpbb/console/command/user/reclean.php
@@ -57,6 +57,7 @@ class reclean extends command
 		$this
 			->setName('user:reclean')
 			->setDescription($this->language->lang('CLI_DESCRIPTION_USER_RECLEAN'))
+			->setHelp($this->language->lang('CLI_HELP_USER_RECLEAN'))
 		;
 	}
 

From 4b789c041844396f3a5e6a51142c45c13d2edd59 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sun, 27 Mar 2016 10:24:12 -0700
Subject: [PATCH 08/12] [ticket/14561] Use the user loader where appropriate

PHPBB3-14561
---
 .../default/container/services_console.yml    |  2 ++
 phpBB/phpbb/console/command/user/activate.php | 31 +++++++------------
 phpBB/phpbb/console/command/user/delete.php   | 31 +++++++------------
 tests/console/user/activate_test.php          |  1 +
 tests/console/user/base.php                   |  3 ++
 tests/console/user/delete_test.php            |  1 +
 6 files changed, 29 insertions(+), 40 deletions(-)

diff --git a/phpBB/config/default/container/services_console.yml b/phpBB/config/default/container/services_console.yml
index 994ac55ee9..3f27ee666a 100644
--- a/phpBB/config/default/container/services_console.yml
+++ b/phpBB/config/default/container/services_console.yml
@@ -229,6 +229,7 @@ services:
             - '@language'
             - '@log'
             - '@notification_manager'
+            - '@user_loader'
             - '%core.root_path%'
             - '%core.php_ext%'
         tags:
@@ -254,6 +255,7 @@ services:
             - '@dbal.conn'
             - '@language'
             - '@log'
+            - '@user_loader'
             - '%core.root_path%'
             - '%core.php_ext%'
         tags:
diff --git a/phpBB/phpbb/console/command/user/activate.php b/phpBB/phpbb/console/command/user/activate.php
index 9eab06847c..5c36da6891 100644
--- a/phpBB/phpbb/console/command/user/activate.php
+++ b/phpBB/phpbb/console/command/user/activate.php
@@ -20,6 +20,7 @@ use phpbb\language\language;
 use phpbb\log\log_interface;
 use phpbb\notification\manager;
 use phpbb\user;
+use phpbb\user_loader;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -43,6 +44,9 @@ class activate extends command
 	/** @var manager */
 	protected $notifications;
 
+	/** @var user_loader */
+	protected $user_loader;
+
 	/**
 	 * phpBB root path
 	 *
@@ -66,16 +70,18 @@ class activate extends command
 	 * @param language         $language
 	 * @param log_interface    $log
 	 * @param manager          $notifications
+	 * @param user_loader      $user_loader
 	 * @param string           $phpbb_root_path
 	 * @param string           $php_ext
 	 */
-	public function __construct(user $user, driver_interface $db, config $config, language $language, log_interface $log, manager $notifications, $phpbb_root_path, $php_ext)
+	public function __construct(user $user, driver_interface $db, config $config, language $language, log_interface $log, manager $notifications, user_loader $user_loader, $phpbb_root_path, $php_ext)
 	{
 		$this->db = $db;
 		$this->config = $config;
 		$this->language = $language;
 		$this->log = $log;
 		$this->notifications = $notifications;
+		$this->user_loader = $user_loader;
 		$this->phpbb_root_path = $phpbb_root_path;
 		$this->php_ext = $php_ext;
 
@@ -132,7 +138,10 @@ class activate extends command
 		$name = $input->getArgument('username');
 		$mode = ($input->getOption('deactivate')) ? 'deactivate' : 'activate';
 
-		if (!$user_row = $this->get_user_data($name))
+		$user_id  = $this->user_loader->load_user_by_username($name);
+		$user_row = $this->user_loader->get_user($user_id);
+
+		if ($user_row['user_id'] == ANONYMOUS)
 		{
 			$io->error($this->language->lang('NO_USER'));
 			return 1;
@@ -207,22 +216,4 @@ class activate extends command
 			$messenger->send(NOTIFY_EMAIL);
 		}
 	}
-
-	/**
-	 * Get the user's data from the database
-	 *
-	 * @param string $name A user name
-	 * @return mixed The user's data array if they exist, false otherwise.
-	 */
-	protected function get_user_data($name)
-	{
-		$sql = 'SELECT *
-			FROM ' . USERS_TABLE . "
-			WHERE username_clean = '" . $this->db->sql_escape(utf8_clean_string($name)) . "'";
-		$result = $this->db->sql_query_limit($sql, 1);
-		$user_row = $this->db->sql_fetchrow($result);
-		$this->db->sql_freeresult($result);
-
-		return $user_row;
-	}
 }
diff --git a/phpBB/phpbb/console/command/user/delete.php b/phpBB/phpbb/console/command/user/delete.php
index 93e75d365b..8593541c1a 100644
--- a/phpBB/phpbb/console/command/user/delete.php
+++ b/phpBB/phpbb/console/command/user/delete.php
@@ -18,6 +18,7 @@ use phpbb\db\driver\driver_interface;
 use phpbb\language\language;
 use phpbb\log\log_interface;
 use phpbb\user;
+use phpbb\user_loader;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -36,6 +37,9 @@ class delete extends command
 	/** @var log_interface */
 	protected $log;
 
+	/** @var user_loader */
+	protected $user_loader;
+
 	/**
 	 * phpBB root path
 	 *
@@ -57,14 +61,16 @@ class delete extends command
 	 * @param driver_interface $db
 	 * @param language         $language
 	 * @param log_interface    $log
+	 * @param user_loader      $user_loader
 	 * @param string           $phpbb_root_path
 	 * @param string           $php_ext
 	 */
-	public function __construct(user $user, driver_interface $db, language $language, log_interface $log, $phpbb_root_path, $php_ext)
+	public function __construct(user $user, driver_interface $db, language $language, log_interface $log, user_loader $user_loader, $phpbb_root_path, $php_ext)
 	{
 		$this->db = $db;
 		$this->language = $language;
 		$this->log = $log;
+		$this->user_loader = $user_loader;
 		$this->phpbb_root_path = $phpbb_root_path;
 		$this->php_ext = $php_ext;
 
@@ -116,7 +122,10 @@ class delete extends command
 		{
 			$io = new SymfonyStyle($input, $output);
 
-			if (!$user_row = $this->get_user_data($name))
+			$user_id  = $this->user_loader->load_user_by_username($name);
+			$user_row = $this->user_loader->get_user($user_id);
+
+			if ($user_row['user_id'] == ANONYMOUS)
 			{
 				$io->error($this->language->lang('NO_USER'));
 				return 1;
@@ -158,22 +167,4 @@ class delete extends command
 			$input->setArgument('username', false);
 		}
 	}
-
-	/**
-	 * Get the user's data from the database
-	 *
-	 * @param string $name A user name
-	 * @return mixed The user's id and username if they exist, false otherwise.
-	 */
-	protected function get_user_data($name)
-	{
-		$sql = 'SELECT user_id, username
-			FROM ' . USERS_TABLE . "
-			WHERE username_clean = '" . $this->db->sql_escape(utf8_clean_string($name)) . "'";
-		$result = $this->db->sql_query_limit($sql, 1);
-		$user_row = $this->db->sql_fetchrow($result);
-		$this->db->sql_freeresult($result);
-
-		return $user_row;
-	}
 }
diff --git a/tests/console/user/activate_test.php b/tests/console/user/activate_test.php
index 08b25c6c95..1588a76e47 100644
--- a/tests/console/user/activate_test.php
+++ b/tests/console/user/activate_test.php
@@ -40,6 +40,7 @@ class phpbb_console_user_activate_test extends phpbb_console_user_base
 			$this->language,
 			$this->log,
 			$this->notifications,
+			$this->user_loader,
 			$this->phpbb_root_path,
 			$this->php_ext
 		));
diff --git a/tests/console/user/base.php b/tests/console/user/base.php
index c6ffc428ed..7ff11af1ae 100644
--- a/tests/console/user/base.php
+++ b/tests/console/user/base.php
@@ -25,6 +25,7 @@ abstract class phpbb_console_user_base extends phpbb_database_test_case
 	protected $passwords_manager;
 	protected $command_name;
 	protected $question;
+	protected $user_loader;
 	protected $phpbb_root_path;
 	protected $php_ext;
 
@@ -70,6 +71,8 @@ abstract class phpbb_console_user_base extends phpbb_database_test_case
 			'\phpbb\datetime'
 		));
 
+		$this->user_loader = new \phpbb\user_loader($db, $phpbb_root_path, $phpEx, USERS_TABLE);
+
 		$driver_helper = new \phpbb\passwords\driver\helper($this->config);
 		$passwords_drivers = array(
 			'passwords.driver.bcrypt_2y'	=> new \phpbb\passwords\driver\bcrypt_2y($this->config, $driver_helper),
diff --git a/tests/console/user/delete_test.php b/tests/console/user/delete_test.php
index dc4c07e8b8..88f91afab1 100644
--- a/tests/console/user/delete_test.php
+++ b/tests/console/user/delete_test.php
@@ -27,6 +27,7 @@ class phpbb_console_user_delete_test extends phpbb_console_user_base
 			$this->db,
 			$this->language,
 			$this->log,
+			$this->user_loader,
 			$this->phpbb_root_path,
 			$this->php_ext
 		));

From afb69d7cd280df65b22b1a338d3023aebf2e3f0c Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sun, 27 Mar 2016 11:25:19 -0700
Subject: [PATCH 09/12] [ticket/14561] Add a progress bar to reclean command

PHPBB3-14561
---
 phpBB/language/en/cli.php                    |  3 +-
 phpBB/phpbb/console/command/user/reclean.php | 76 +++++++++++++++++++-
 tests/console/user/reclean_test.php          |  7 +-
 3 files changed, 78 insertions(+), 8 deletions(-)

diff --git a/phpBB/language/en/cli.php b/phpBB/language/en/cli.php
index 872aa464b5..db4b5f9ec6 100644
--- a/phpBB/language/en/cli.php
+++ b/phpBB/language/en/cli.php
@@ -136,7 +136,8 @@ $lang = array_merge($lang, array(
 
 	'CLI_USER_ADD_SUCCESS'		=> 'Successfully added user %s.',
 	'CLI_USER_DELETE_CONFIRM'	=> 'Are you sure you want to delete ‘%s’? [y/N]',
-	'CLI_USER_RECLEAN_SUCCESS'	=> [
+	'CLI_USER_RECLEAN_START'	=> 'Re-cleaning usernames',
+	'CLI_USER_RECLEAN_DONE'		=> [
 		0	=> 'Re-cleaning complete. No usernames needed to be cleaned.',
 		1	=> 'Re-cleaning complete. %d username was cleaned.',
 		2	=> 'Re-cleaning complete. %d usernames were cleaned.',
diff --git a/phpBB/phpbb/console/command/user/reclean.php b/phpBB/phpbb/console/command/user/reclean.php
index ba8a638e7b..20c2816be5 100644
--- a/phpBB/phpbb/console/command/user/reclean.php
+++ b/phpBB/phpbb/console/command/user/reclean.php
@@ -17,6 +17,7 @@ use phpbb\console\command\command;
 use phpbb\db\driver\driver_interface;
 use phpbb\language\language;
 use phpbb\user;
+use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
@@ -32,6 +33,9 @@ class reclean extends command
 	/** @var int A count of the number of re-cleaned user names */
 	protected $processed;
 
+	/** @var ProgressBar */
+	protected $progress;
+
 	/**
 	 * Construct method
 	 *
@@ -73,16 +77,27 @@ class reclean extends command
 	 */
 	protected function execute(InputInterface $input, OutputInterface $output)
 	{
+		$io = new SymfonyStyle($input, $output);
+
+		$io->section($this->language->lang('CLI_USER_RECLEAN_START'));
+
 		$this->processed = 0;
 
+		$this->progress = $this->create_progress_bar($this->get_count(), $io, $output);
+		$this->progress->setMessage($this->language->lang('CLI_USER_RECLEAN_START'));
+		$this->progress->start();
+
 		$stage = 0;
 		while ($stage !== true)
 		{
 			$stage = $this->reclean_usernames($stage);
 		}
 
-		$io = new SymfonyStyle($input, $output);
-		$io->success($this->language->lang('CLI_USER_RECLEAN_SUCCESS', $this->processed));
+		$this->progress->finish();
+
+		$io->newLine(2);
+		$io->success($this->language->lang('CLI_USER_RECLEAN_DONE', $this->processed));
+
 		return 0;
 	}
 
@@ -116,6 +131,8 @@ class reclean extends command
 
 				$this->processed++;
 			}
+
+			$this->progress->advance();
 		}
 		$this->db->sql_freeresult($result);
 
@@ -123,4 +140,59 @@ class reclean extends command
 
 		return ($i < $limit) ? true : $start + $i;
 	}
+
+	/**
+	 * Create a styled progress bar
+	 *
+	 * @param integer         $max    Max value for the progress bar
+	 * @param SymfonyStyle    $io
+	 * @param OutputInterface $output The output stream, used to print messages
+	 * @return ProgressBar
+	 */
+	protected function create_progress_bar($max, SymfonyStyle $io, OutputInterface $output)
+	{
+		$progress = $io->createProgressBar($max);
+		if ($output->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE)
+		{
+			$progress->setFormat('<info>[%percent:3s%%]</info> %message%');
+			$progress->setOverwrite(false);
+		}
+		else if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE)
+		{
+			$progress->setFormat('<info>[%current:s%/%max:s%]</info><comment>[%elapsed%/%estimated%][%memory%]</comment> %message%');
+			$progress->setOverwrite(false);
+		}
+		else
+		{
+			$io->newLine(2);
+			$progress->setFormat(
+				"    %current:s%/%max:s% %bar%  %percent:3s%%\n" .
+				"        %message% %elapsed:6s%/%estimated:-6s% %memory:6s%\n");
+			$progress->setBarWidth(60);
+		}
+
+		if (!defined('PHP_WINDOWS_VERSION_BUILD'))
+		{
+			$progress->setEmptyBarCharacter('░'); // light shade character \u2591
+			$progress->setProgressCharacter('');
+			$progress->setBarCharacter('▓'); // dark shade character \u2593
+		}
+
+		return $progress;
+	}
+
+	/**
+	 * Get the count of users in the database
+	 *
+	 * @return int
+	 */
+	protected function get_count()
+	{
+		$sql = 'SELECT COUNT(user_id) AS count FROM ' . USERS_TABLE;
+		$result = $this->db->sql_query($sql);
+		$count = (int) $this->db->sql_fetchfield('count');
+		$this->db->sql_freeresult($result);
+
+		return $count;
+	}
 }
diff --git a/tests/console/user/reclean_test.php b/tests/console/user/reclean_test.php
index 2b28484084..1bf0b8ef5a 100644
--- a/tests/console/user/reclean_test.php
+++ b/tests/console/user/reclean_test.php
@@ -38,11 +38,8 @@ class phpbb_console_user_reclean_test extends phpbb_console_user_base
 	{
 		$command_tester = $this->get_command_tester();
 
-		$command_tester->execute(array(
-			'command' => $this->command_name,
-		));
-
-		$this->assertContains('CLI_USER_RECLEAN_SUCCESS', $command_tester->getDisplay());
+		$exit_status = $command_tester->execute(array('command' => $this->command_name));
+		$this->assertSame(0, $exit_status);
 
 		$result = $this->db->sql_query('SELECT user_id FROM ' . USERS_TABLE . " WHERE username_clean = 'test unclean'");
 		$row = $this->db->sql_fetchrow($result);

From e81bf76dea82c4bc98ab7214a656a093c67f25dd Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Sun, 27 Mar 2016 11:30:05 -0700
Subject: [PATCH 10/12] [ticket/14561] Fix function docblock in activate
 command

PHPBB3-14561
---
 phpBB/phpbb/console/command/user/activate.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/phpBB/phpbb/console/command/user/activate.php b/phpBB/phpbb/console/command/user/activate.php
index 5c36da6891..9c85718b4c 100644
--- a/phpBB/phpbb/console/command/user/activate.php
+++ b/phpBB/phpbb/console/command/user/activate.php
@@ -123,8 +123,7 @@ class activate extends command
 	/**
 	 * Executes the command user:activate
 	 *
-	 * Deletes a user from the database. An option to delete the user's posts
-	 * is available, by default posts will be retained.
+	 * Activate (or deactivate) a user account
 	 *
 	 * @param InputInterface  $input  The input stream used to get the options
 	 * @param OutputInterface $output The output stream, used to print messages

From 2b90591a317fd75c7c8c4cf690d7209935f3e810 Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Mon, 28 Mar 2016 15:26:56 -0700
Subject: [PATCH 11/12] [ticket/14561] Small change to progress bar output

PHPBB3-14561
---
 phpBB/phpbb/console/command/user/reclean.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/phpBB/phpbb/console/command/user/reclean.php b/phpBB/phpbb/console/command/user/reclean.php
index 20c2816be5..e298c285be 100644
--- a/phpBB/phpbb/console/command/user/reclean.php
+++ b/phpBB/phpbb/console/command/user/reclean.php
@@ -167,7 +167,7 @@ class reclean extends command
 			$io->newLine(2);
 			$progress->setFormat(
 				"    %current:s%/%max:s% %bar%  %percent:3s%%\n" .
-				"        %message% %elapsed:6s%/%estimated:-6s% %memory:6s%\n");
+				"                         %elapsed:6s%/%estimated:-6s% %memory:6s%\n");
 			$progress->setBarWidth(60);
 		}
 

From 1641fd18c142ede0df8d7486a33bf0b7bd449f2c Mon Sep 17 00:00:00 2001
From: Matt Friedman <maf675@gmail.com>
Date: Thu, 31 Mar 2016 15:14:41 -0700
Subject: [PATCH 12/12] [ticket/14561] Remove requires for functions in tests

PHPBB3-14561
---
 tests/console/user/base.php | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/tests/console/user/base.php b/tests/console/user/base.php
index 7ff11af1ae..b84c0bb267 100644
--- a/tests/console/user/base.php
+++ b/tests/console/user/base.php
@@ -11,10 +11,6 @@
 *
 */
 
-require_once dirname(__FILE__) . '/../../../phpBB/includes/functions_user.php';
-require_once dirname(__FILE__) . '/../../../phpBB/includes/functions.php';
-require_once dirname(__FILE__) . '/../../../phpBB/includes/utf/utf_tools.php';
-
 abstract class phpbb_console_user_base extends phpbb_database_test_case
 {
 	protected $db;