From 5ae1d9eac68fc9586b7006a692f396f5c4e81804 Mon Sep 17 00:00:00 2001 From: Oliver Schramm Date: Fri, 28 Sep 2018 17:09:31 +0200 Subject: [PATCH] [ticket/9687] Introduce new ban manager (WIP) PHPBB3-9687 --- .../exception/invalid_length_exception.php | 20 ++ .../exception/type_not_found_exception.php | 20 ++ phpBB/phpbb/ban/manager.php | 160 ++++++++++++++++ phpBB/phpbb/ban/type/base.php | 95 ++++++++++ phpBB/phpbb/ban/type/email.php | 63 +++++++ phpBB/phpbb/ban/type/type_interface.php | 73 ++++++++ phpBB/phpbb/ban/type/user.php | 78 ++++++++ .../db/migration/data/v330/ban_table_p1.php | 174 ++++++++++++++++++ .../db/migration/data/v330/ban_table_p2.php | 59 ++++++ 9 files changed, 742 insertions(+) create mode 100644 phpBB/phpbb/ban/exception/invalid_length_exception.php create mode 100644 phpBB/phpbb/ban/exception/type_not_found_exception.php create mode 100644 phpBB/phpbb/ban/manager.php create mode 100644 phpBB/phpbb/ban/type/base.php create mode 100644 phpBB/phpbb/ban/type/email.php create mode 100644 phpBB/phpbb/ban/type/type_interface.php create mode 100644 phpBB/phpbb/ban/type/user.php create mode 100644 phpBB/phpbb/db/migration/data/v330/ban_table_p1.php create mode 100644 phpBB/phpbb/db/migration/data/v330/ban_table_p2.php diff --git a/phpBB/phpbb/ban/exception/invalid_length_exception.php b/phpBB/phpbb/ban/exception/invalid_length_exception.php new file mode 100644 index 0000000000..dbbf09f0ac --- /dev/null +++ b/phpBB/phpbb/ban/exception/invalid_length_exception.php @@ -0,0 +1,20 @@ + + * @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\ban\exception; + +use phpbb\exception\runtime_exception; + +class invalid_length_exception extends runtime_exception +{ +} diff --git a/phpBB/phpbb/ban/exception/type_not_found_exception.php b/phpBB/phpbb/ban/exception/type_not_found_exception.php new file mode 100644 index 0000000000..a4d7e8d19f --- /dev/null +++ b/phpBB/phpbb/ban/exception/type_not_found_exception.php @@ -0,0 +1,20 @@ + + * @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\ban\exception; + +use phpbb\exception\runtime_exception; + +class type_not_found_exception extends runtime_exception +{ +} diff --git a/phpBB/phpbb/ban/manager.php b/phpBB/phpbb/ban/manager.php new file mode 100644 index 0000000000..46b46b78b7 --- /dev/null +++ b/phpBB/phpbb/ban/manager.php @@ -0,0 +1,160 @@ + + * @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\ban; + +use phpbb\ban\exception\invalid_length_exception; +use phpbb\ban\exception\type_not_found_exception; + +class manager +{ + protected $ban_table; + + protected $db; + + protected $log; + + protected $sessions_keys_table; + + protected $sessions_table; + + protected $types; + + protected $user; + + protected $users_table; + + public function __construct($types, \phpbb\db\driver\driver_interface $db, \phpbb\log\log_interface $log, \phpbb\user $user, $ban_table, $users_table = '', $sessions_table = '', $sessions_keys_table = '') + { + $this->ban_table = $ban_table; + $this->db = $db; + $this->log = $log; + $this->sessions_keys_table = $sessions_keys_table; + $this->sessions_table = $sessions_table; + $this->types = $types; + $this->user = $user; + $this->users_table = $users_table; + } + + public function ban($mode, array $items, \DateTimeInterface $start, \DateTimeInterface $end, $reason, $display_reason = '', $logging = true) + { + if (!isset($this->types[$mode])) + { + throw new type_not_found_exception(); // TODO + } + if ($start > $end && $end->getTimestamp() !== 0) + { + throw new invalid_length_exception(); // TODO + } + + /** @var \phpbb\ban\type\type_interface $ban_mode */ + $ban_mode = $this->types[$mode]; + $ban_items = $ban_mode->prepare_for_storage($items); + + // Prevent duplicate bans + $sql = 'DELETE FROM ' . $this->ban_table . " + WHERE ban_mode = '" . $this->db->sql_escape($mode) . "' + AND " . $this->db->sql_in_set('ban_item', $ban_items); + $this->db->sql_query($sql); + + $insert_array = []; + foreach ($ban_items as $ban_item) + { + $insert_array[] = [ + 'ban_mode' => $mode, + 'ban_item' => $ban_item, + 'ban_start' => $start->getTimestamp(), + 'ban_end' => $end->getTimestamp(), + 'ban_reason' => $reason, + 'ban_reason_display' => $display_reason, + ]; + } + + if (empty($insert_array)) + { + return; + } + + $result = $this->db->sql_multi_insert($this->ban_table, $insert_array); + if ($result === false) + { + // Something went wrong + // TODO throw exception + } + + if ($logging) + { + // TODO logging + } + + if (!$ban_mode->after_ban()) + { + return; + } + + $user_column = $ban_mode->get_user_column(); + if (!empty($user_column) && !empty($this->users_table)) + { + $ban_items_sql = []; + $ban_or_like = ''; + foreach ($ban_items as $ban_item) + { + if (stripos($ban_item, '*') === false) + { + $ban_items_sql[] = $ban_item; + } + else + { + $ban_or_like .= ' OR ' . $user_column . ' ' . $this->db->sql_like_expression(str_replace('*', $this->db->get_any_char(), $ban_item)); + } + } + + $sql = 'SELECT user_id + FROM ' . $this->users_table . ' + WHERE ' . $this->db->sql_in_set('u.' . $user_column, $ban_items) . $ban_or_like; + $result = $this->db->sql_query($sql); + + $user_ids = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $user_ids[] = (int)$row['user_id']; + } + $this->db->sql_freeresult($result); + + if (!empty($user_ids) && !empty($this->sessions_table)) + { + $sql = 'DELETE FROM ' . $this->sessions_table . ' + WHERE ' . $this->db->sql_in_set('session_user_id', $user_ids); + $this->db->sql_query($sql); + } + if (!empty($user_ids) && !empty($this->sessions_keys_table)) + { + $sql = 'DELETE FROM ' . $this->sessions_keys_table . ' + WHERE ' . $this->db->sql_in_set('user_id', $user_ids); + $this->db->sql_query($sql); + } + } + } + + public function unban($mode, array $items, $reason, $logging = true) + { + } + + public function check(array $user_data = []) + { + } + + public function tidy() + { + } +} diff --git a/phpBB/phpbb/ban/type/base.php b/phpBB/phpbb/ban/type/base.php new file mode 100644 index 0000000000..7f49d1cc66 --- /dev/null +++ b/phpBB/phpbb/ban/type/base.php @@ -0,0 +1,95 @@ + + * @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\ban\type; + +abstract class base implements type_interface +{ + /** @var \phpbb\db\driver\driver_interface */ + protected $db; + + /** @var array */ + protected $excluded; + + /** @var string */ + protected $users_table; + + public function __construct(\phpbb\db\driver\driver_interface $db, $users_table) + { + $this->db = $db; + $this->users_table = $users_table; + } + + /** + * {@inheritDoc} + */ + public function get_user_column() + { + return null; + } + + /** + * {@inheritDoc} + */ + public function after_ban() + { + return true; + } + + public function after_unban() + { + } + + /** + * {@inheritDoc} + */ + public function check(array $data) + { + return false; + } + + public function tidy() + { + } + + /** + * Queries users that are excluded from banning (like founders) + * from the database and saves them in $this->excluded array. + * Returns true on success and false on failure + * + * @return bool + */ + protected function get_excluded() + { + $user_column = $this->get_user_column(); + if (empty($user_column)) + { + return false; + } + + $this->excluded = []; + + $sql = "SELECT user_id, {$user_column} + FROM {$this->users_table} + WHERE user_type = " . USER_FOUNDER; + $result = $this->db->sql_query($sql); + + while ($row = $this->db->sql_fetchrow($result)) + { + $this->excluded[(int) $row['user_id']] = $row[$user_column]; + } + $this->db->sql_freeresult($result); + + return true; + } +} diff --git a/phpBB/phpbb/ban/type/email.php b/phpBB/phpbb/ban/type/email.php new file mode 100644 index 0000000000..4c95292c7a --- /dev/null +++ b/phpBB/phpbb/ban/type/email.php @@ -0,0 +1,63 @@ + + * @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\ban\type; + +class email extends base +{ + /** + * {@inheritDoc} + */ + public function get_type() + { + return 'email'; + } + + /** + * {@inheritDoc} + */ + public function get_user_column() + { + return 'user_email'; + } + + /** + * {@inheritDoc} + */ + public function prepare_for_storage(array $items) + { + if (!$this->get_excluded()) + { + // TODO throw exception + } + $regex = get_preg_expression('email'); + + $ban_items = []; + foreach ($items as $item) + { + $item = trim($item); + if (strlen($item) > 100 || preg_match($regex, $item) || in_array($item, $this->excluded)) + { + continue; + } + $ban_items[] = $item; + } + + if (empty($ban_items)) + { + // TODO throw exception - no valid emails defined + } + + return $ban_items; + } +} diff --git a/phpBB/phpbb/ban/type/type_interface.php b/phpBB/phpbb/ban/type/type_interface.php new file mode 100644 index 0000000000..a13681d2e1 --- /dev/null +++ b/phpBB/phpbb/ban/type/type_interface.php @@ -0,0 +1,73 @@ + + * @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\ban\type; + +/** + * Interface implemented by all ban types + */ +interface type_interface +{ + /** + * Returns the type identifier for this ban type + * + * @return string + */ + public function get_type(); + + /** + * Returns the column in the users table which contains + * the values that should be looked for when checking a ban. + * If it returns null, the check method will be called when + * checking for bans. + * + * @return string|null + */ + public function get_user_column(); + + /** + * Gives the possibility to do some clean up after banning + * Returns true if affected users should be logged out and + * false otherwise + * + * @return bool + */ + public function after_ban(); + + public function after_unban(); // ??? + + /** + * In the case that get_user_column() returns null, this method + * is called when checking the ban status. + * Please note, that this method is basically called on every page, + * so the check should perform rather fast. + * + * Returns true if the person is banned and false otherwise. + * + * @param array $data The user data array + * + * @return bool + */ + public function check(array $data); + + /** + * Prepares the given ban items before saving them in the database + * + * @param array $items + * + * @return array + */ + public function prepare_for_storage(array $items); + + public function tidy(); // ??? +} diff --git a/phpBB/phpbb/ban/type/user.php b/phpBB/phpbb/ban/type/user.php new file mode 100644 index 0000000000..dec17fd673 --- /dev/null +++ b/phpBB/phpbb/ban/type/user.php @@ -0,0 +1,78 @@ + + * @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\ban\type; + +class user extends base +{ + public function get_type() + { + return 'user'; + } + + public function get_user_column() + { + return 'user_id'; + } + + public function prepare_for_storage(array $items) + { + if (!$this->get_excluded()) + { + // TODO throw exception + } + + $sql_usernames = []; + $sql_or_like = []; + foreach ($items as $item) + { + $cleaned_username = utf8_clean_string($item); + if (stripos($cleaned_username, '*') === false) + { + $sql_usernames[] = $cleaned_username; + } + else + { + $sql_or_like[] = ['username_clean', 'LIKE', str_replace('*', $this->db->get_any_char(), $cleaned_username)]; + } + } + + $sql_array = [ + 'SELECT' => 'user_id', + 'FROM' => [ + $this->users_table => '', + ], + 'WHERE' => ['AND', + [ + ['OR', + array_merge([ + ['username_clean', 'IN', $sql_usernames] + ], $sql_or_like), + ], + ['user_id', 'NOT_IN', array_map('intval', $this->excluded)], + ], + ], + ]; + $sql = $this->db->sql_build_query('SELECT', $sql_array); + $result = $this->db->sql_query($sql); + + $ban_items = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $ban_items[] = (int) $row['user_id']; + } + $this->db->sql_freeresult($result); + + return $ban_items; + } +} diff --git a/phpBB/phpbb/db/migration/data/v330/ban_table_p1.php b/phpBB/phpbb/db/migration/data/v330/ban_table_p1.php new file mode 100644 index 0000000000..2b2e027a49 --- /dev/null +++ b/phpBB/phpbb/db/migration/data/v330/ban_table_p1.php @@ -0,0 +1,174 @@ + + * @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\db\migration\data\v330; + +class ban_table_p1 extends \phpbb\db\migration\migration +{ + static public function depends_on() + { + return array('\phpbb\db\migration\data\v320\default_data_type_ids'); + } + + public function update_schema() + { + return array( + 'add_tables' => array( + $this->table_prefix . 'bans' => array( + 'COLUMNS' => array( + 'ban_id' => array('ULINT', null, 'auto_increment'), + 'ban_mode' => array('VCHAR', ''), + 'ban_item' => array('STEXT_UNI', ''), + 'ban_start' => array('TIMESTAMP', 0), + 'ban_end' => array('TIMESTAMP', 0), + 'ban_reason' => array('VCHAR_UNI', ''), + 'ban_reason_display' => array('VCHAR_UNI', ''), + ), + 'PRIMARY_KEY' => 'ban_id', + 'KEYS' => array( + 'ban_end' => array('INDEX', 'ban_end'), + ), + ), + ), + ); + } + + public function revert_schema() + { + return array( + 'drop_tables' => array( + $this->table_prefix . 'bans', + ), + ); + } + + public function update_data() + { + return array( + array('custom', array(array($this, 'old_to_new'))), + ); + } + + public function revert_data() + { + return array( + array('custom', array(array($this, 'new_to_old'))), + ); + } + + public function old_to_new($start) + { + $start = (int) $start; + $limit = 500; + $processed_rows = 0; + + $sql = 'SELECT * + FROM ' . $this->table_prefix . "banlist"; + $result = $this->db->sql_query_limit($sql, $limit, $start); + + $bans = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $processed_rows++; + + if ($row['ban_exclude']) + { + continue; + } + + $row['ban_userid'] = (int) $row['ban_userid']; + $item = $mode = ''; + if ($row['ban_ip'] !== '') + { + $mode = 'ip'; + $item = $row['ban_ip']; + } + else if ($row['ban_email'] !== '') + { + $mode = 'email'; + $item = $row['ban_email']; + } + else if ($row['ban_userid'] !== 0) + { + $mode = 'user'; + $item = $row['ban_userid']; + } + + if ($mode === '' || $item === '') + { + continue; + } + + $bans[] = [ + 'ban_mode' => $mode, + 'ban_item' => $item, + 'ban_start' => $row['ban_start'], + 'ban_end' => $row['ban_end'], + 'ban_reason' => $row['ban_reason'], + 'ban_reason_display' => $row['ban_give_reason'], + ]; + } + $this->db->sql_freeresult($result); + + if ($processed_rows > 0) + { + $this->db->sql_multi_insert($this->table_prefix . 'bans', $bans); + } + else if ($processed_rows < $limit) + { + return; + } + + return $limit + $start; + } + + public function new_to_old($start) + { + $start = (int) $start; + $limit = 500; + $processed_rows = 0; + + $sql = 'SELECT * + FROM ' . $this->table_prefix . "bans"; + $result = $this->db->sql_query_limit($sql, $limit, $start); + + $bans = []; + while ($row = $this->db->sql_fetchrow($result)) + { + $processed_rows++; + + $bans[] = [ + 'ban_userid' => ($row['ban_mode'] === 'user') ? (int)$row['ban_item'] : 0, + 'ban_ip' => ($row['ban_mode'] === 'ip') ? $row['ban_item'] : '', + 'ban_email' => ($row['ban_mode'] === 'email') ? $row['ban_item'] : '', + 'ban_start' => $row['ban_start'], + 'ban_end' => $row['ban_end'], + 'ban_exclude' => false, + 'ban_reason' => $row['ban_reason'], + 'ban_give_reason' => $row['ban_reason_display'], + ]; + } + $this->db->sql_freeresult($result); + + if ($processed_rows > 0) + { + $this->db->sql_multi_insert($this->table_prefix . 'banlist', $bans); + } + else if ($processed_rows < $limit) + { + return; + } + + return $limit + $start; + } +} diff --git a/phpBB/phpbb/db/migration/data/v330/ban_table_p2.php b/phpBB/phpbb/db/migration/data/v330/ban_table_p2.php new file mode 100644 index 0000000000..de617a05f2 --- /dev/null +++ b/phpBB/phpbb/db/migration/data/v330/ban_table_p2.php @@ -0,0 +1,59 @@ + + * @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\db\migration\data\v330; + +class ban_table_p2 extends \phpbb\db\migration\migration +{ + static public function depends_on() + { + return array('\phpbb\db\migration\data\v330\ban_table_p1'); + } + + public function update_schema() + { + return array( + 'drop_tables' => array( + $this->table_prefix . 'banlist', + ), + ); + } + + public function revert_schema() + { + return array( + 'add_tables' => array( + $this->table_prefix . 'banlist' => array( + 'COLUMNS' => array( + 'ban_id' => array('ULINT', NULL, 'auto_increment'), + 'ban_userid' => array('ULINT', 0), + 'ban_ip' => array('VCHAR:40', ''), + 'ban_email' => array('VCHAR_UNI:100', ''), + 'ban_start' => array('TIMESTAMP', 0), + 'ban_end' => array('TIMESTAMP', 0), + 'ban_exclude' => array('BOOL', 0), + 'ban_reason' => array('VCHAR_UNI', ''), + 'ban_give_reason' => array('VCHAR_UNI', ''), + ), + 'PRIMARY_KEY' => 'ban_id', + 'KEYS' => array( + 'ban_end' => array('INDEX', 'ban_end'), + 'ban_user' => array('INDEX', array('ban_userid', 'ban_exclude')), + 'ban_email' => array('INDEX', array('ban_email', 'ban_exclude')), + 'ban_ip' => array('INDEX', array('ban_ip', 'ban_exclude')), + ), + ), + ), + ); + } +}