From ffd0fc66a759b39b2c29eb9c9326b2b2792d66d8 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 30 Apr 2026 08:13:20 -0700 Subject: [PATCH 1/2] Implement CSV export of consent logs --- acp/consentmanager_info.php | 5 + acp/consentmanager_module.php | 16 +- adm/style/consentmanager_acp_export.html | 50 ++++ config/services.yml | 10 +- controller/acp_controller.php | 152 ++++++++-- language/en/acp_consentmanager.php | 15 + language/en/info_acp_consentmanager.php | 2 + migrations/m2_export_module.php | 52 ++++ service/acp_manager.php | 224 +++++++++++++++ service/log_manager.php | 28 +- tests/acp/acp_module_test.php | 34 +++ tests/controller/acp_controller_test.php | 275 +++++++++++++++++- tests/service/acp_manager_test.php | 347 +++++++++++++++++++++++ tests/service/log_manager_test.php | 38 +-- 14 files changed, 1148 insertions(+), 100 deletions(-) create mode 100644 adm/style/consentmanager_acp_export.html create mode 100644 migrations/m2_export_module.php create mode 100644 service/acp_manager.php create mode 100644 tests/service/acp_manager_test.php diff --git a/acp/consentmanager_info.php b/acp/consentmanager_info.php index 357ac61..5506396 100644 --- a/acp/consentmanager_info.php +++ b/acp/consentmanager_info.php @@ -23,6 +23,11 @@ public function module() 'auth' => 'ext_phpbb/consentmanager && acl_a_board', 'cat' => ['ACP_CONSENTMANAGER'], ], + 'export' => [ + 'title' => 'ACP_CONSENTMANAGER_EXPORT', + 'auth' => 'ext_phpbb/consentmanager && acl_a_board', + 'cat' => ['ACP_CONSENTMANAGER'], + ], ], ]; } diff --git a/acp/consentmanager_module.php b/acp/consentmanager_module.php index 14530c9..e03c307 100644 --- a/acp/consentmanager_module.php +++ b/acp/consentmanager_module.php @@ -24,9 +24,19 @@ public function main($id, $mode) $controller = $phpbb_container->get('phpbb.consentmanager.controller.acp'); $controller->set_page_url($this->u_action); - $this->tpl_name = 'consentmanager_acp'; - $this->page_title = 'ACP_CONSENTMANAGER'; + switch ($mode) + { + case 'export': + $this->tpl_name = 'consentmanager_acp_export'; + $this->page_title = 'ACP_CONSENTMANAGER_EXPORT'; + $controller->handle_export(); + break; - $controller->handle(); + default: + $this->tpl_name = 'consentmanager_acp'; + $this->page_title = 'ACP_CONSENTMANAGER'; + $controller->handle(); + break; + } } } diff --git a/adm/style/consentmanager_acp_export.html b/adm/style/consentmanager_acp_export.html new file mode 100644 index 0000000..311a08d --- /dev/null +++ b/adm/style/consentmanager_acp_export.html @@ -0,0 +1,50 @@ +{% include 'overall_header.html' %} + +

{{ lang('ACP_CONSENTMANAGER') }}

+ +

{{ lang('ACP_CONSENTMANAGER_EXPORT_EXPLAIN') }}

+ +{% if S_ERROR %} +
+

{{ lang('WARNING') }}

+

{{ ERROR_MSG }}

+
+{% endif %} + +
+
+ {{ lang('ACP_CONSENTMANAGER_EXPORT_FILTERS') }} +
+
+ +
{{ lang('ACP_CONSENTMANAGER_EXPORT_DATE_EXPLAIN') }} +
+
+
+
+
+
+
+
+
+ +
{{ lang('ACP_CONSENTMANAGER_EXPORT_USER_ID_EXPLAIN') }} +
+
+
+
+
+ +
{{ lang('ACP_CONSENTMANAGER_EXPORT_VERSION_EXPLAIN') }} +
+
+
+
+ +
+ + {{ S_FORM_TOKEN }} +
+
+ +{% include 'overall_footer.html' %} diff --git a/config/services.yml b/config/services.yml index 2719831..dfa23ca 100644 --- a/config/services.yml +++ b/config/services.yml @@ -15,6 +15,14 @@ services: phpbb.consentmanager.log_manager: class: phpbb\consentmanager\service\log_manager + arguments: + - '@config' + - '@dbal.conn' + - '@user' + - '%tables.phpbb.consentmanager.consent_logs%' + + phpbb.consentmanager.acp_manager: + class: phpbb\consentmanager\service\acp_manager arguments: - '@config' - '@dbal.conn' @@ -37,7 +45,7 @@ services: arguments: - '@language' - '@phpbb.consentmanager.service' - - '@phpbb.consentmanager.log_manager' + - '@phpbb.consentmanager.acp_manager' - '@request' - '@template' diff --git a/controller/acp_controller.php b/controller/acp_controller.php index 38f8783..00bf85b 100644 --- a/controller/acp_controller.php +++ b/controller/acp_controller.php @@ -10,8 +10,8 @@ namespace phpbb\consentmanager\controller; +use phpbb\consentmanager\service\acp_manager; use phpbb\consentmanager\service\consent_manager_interface; -use phpbb\consentmanager\service\log_manager; use phpbb\language\language; use phpbb\request\request; use phpbb\template\template; @@ -24,8 +24,8 @@ class acp_controller /** @var consent_manager_interface */ protected $consent_manager; - /** @var log_manager */ - protected $log_manager; + /** @var acp_manager */ + protected $acp_manager; /** @var request */ protected $request; @@ -41,15 +41,15 @@ class acp_controller * * @param language $language Language service * @param consent_manager_interface $consent_manager Consent manager service - * @param log_manager $log_manager Consent log manager + * @param acp_manager $acp_manager ACP manager service * @param request $request Request service * @param template $template Template service */ - public function __construct(language $language, consent_manager_interface $consent_manager, log_manager $log_manager, request $request, template $template) + public function __construct(language $language, consent_manager_interface $consent_manager, acp_manager $acp_manager, request $request, template $template) { $this->language = $language; $this->consent_manager = $consent_manager; - $this->log_manager = $log_manager; + $this->acp_manager = $acp_manager; $this->request = $request; $this->template = $template; @@ -79,7 +79,7 @@ public function handle() if ($this->request->is_set_post('submit')) { - $this->validate_form_key(); + $this->validate_form_key('phpbb_consentmanager_acp'); $errors = []; $saved = $this->consent_manager->save_acp_settings([ @@ -94,16 +94,16 @@ public function handle() return; } - $this->log_manager->log_admin_settings_updated(); + $this->acp_manager->log_admin_settings_updated(); trigger_error($this->language->lang('CONFIG_UPDATED') . adm_back_link($this->u_action)); } if ($this->request->is_set_post('reset_consent')) { - $this->validate_form_key(); + $this->validate_form_key('phpbb_consentmanager_acp'); $this->consent_manager->reset_consent_version(); - $this->log_manager->log_admin_reprompt(); + $this->acp_manager->log_admin_reprompt(); trigger_error($this->language->lang('ACP_CONSENTMANAGER_REPROMPT_SUCCESS') . adm_back_link($this->u_action)); } @@ -112,12 +112,131 @@ public function handle() } /** - * Assign consent manager settings to the ACP template. + * Handle the ACP export page request. * - * @param array $errors Validation errors to display + * Displays the filter form on GET. On POST with download_csv, streams a + * CSV file of consent log records matching the supplied filters. * * @return void */ + public function handle_export() + { + add_form_key('phpbb_consentmanager_export'); + + if ($this->request->is_set_post('download_csv')) + { + $this->validate_form_key('phpbb_consentmanager_export'); + + $errors = []; + $filters = $this->parse_export_filters($errors); + + if (!empty($errors)) + { + $this->assign_export_template_vars($errors); + return; + } + + $this->acp_manager->log_admin_export(); + $this->send_csv_download($filters); + } + + $this->assign_export_template_vars(); + } + + /** + * Parse and validate filter inputs from the export form. + * + * @param array $errors Reference — validation errors are appended here + * + * @return array Validated filter map (date_from, date_to, user_id, consent_version) + */ + protected function parse_export_filters(array &$errors) + { + $date_from_str = trim($this->request->variable('export_date_from', '')); + $date_to_str = trim($this->request->variable('export_date_to', '')); + $user_id = $this->request->variable('export_user_id', 0); + $consent_ver = $this->request->variable('export_consent_version', 0); + + $filters = []; + $date_from = $this->acp_manager->parse_date_filter($date_from_str); + $date_to = $this->acp_manager->parse_date_filter($date_to_str, true); + + if ($date_from_str !== '' && $date_from === false) + { + $errors[] = $this->language->lang('ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_FROM'); + } + + if ($date_to_str !== '' && $date_to === false) + { + $errors[] = $this->language->lang('ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_TO'); + } + + if ($date_from !== false && $date_to !== false && $date_from > $date_to) + { + $errors[] = $this->language->lang('ACP_CONSENTMANAGER_EXPORT_DATE_RANGE_INVALID'); + } + + if (empty($errors)) + { + if ($date_from !== false) + { + $filters['date_from'] = $date_from; + } + + if ($date_to !== false) + { + $filters['date_to'] = $date_to; + } + } + + if ($user_id > 0) + { + $filters['user_id'] = $user_id; + } + + if ($consent_ver > 0) + { + $filters['consent_version'] = $consent_ver; + } + + return $filters; + } + + protected function send_csv_download(array $filters) + { + if (ob_get_level()) + { + ob_end_clean(); + } + + header('Content-Type: text/csv; charset=UTF-8'); + header('Content-Disposition: attachment; filename="consent_logs.csv"'); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Pragma: no-cache'); + header('Expires: 0'); + + $handle = fopen('php://output', 'w'); + fwrite($handle, "\xEF\xBB\xBF"); // UTF-8 BOM for Excel compatibility + fputcsv($handle, ['anonymized_id', 'timestamp', 'consent_version', 'categories']); + $this->acp_manager->stream_logs_csv($handle, $filters); + fclose($handle); + + exit; + } + + protected function assign_export_template_vars(array $errors = []) + { + $this->template->assign_vars([ + 'S_ERROR' => !empty($errors), + 'ERROR_MSG' => implode('
', $errors), + 'EXPORT_DATE_FROM' => $this->request->variable('export_date_from', ''), + 'EXPORT_DATE_TO' => $this->request->variable('export_date_to', ''), + 'EXPORT_USER_ID' => $this->request->variable('export_user_id', 0), + 'EXPORT_CONSENT_VER' => $this->request->variable('export_consent_version', 0), + 'U_ACTION' => $this->u_action, + ]); + } + protected function assign_template_vars(array $errors = []) { $this->template->assign_vars(array_merge( @@ -130,14 +249,9 @@ protected function assign_template_vars(array $errors = []) )); } - /** - * Ensure the ACP form key is valid before processing changes. - * - * @return void - */ - protected function validate_form_key() + protected function validate_form_key($form_key) { - if (!check_form_key('phpbb_consentmanager_acp')) + if (!check_form_key($form_key)) { trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); } diff --git a/language/en/acp_consentmanager.php b/language/en/acp_consentmanager.php index cde21f8..3c20257 100644 --- a/language/en/acp_consentmanager.php +++ b/language/en/acp_consentmanager.php @@ -34,4 +34,19 @@ 'ACP_CONSENTMANAGER_INVALID_INTEGRATIONS' => 'The integrations field must contain a valid JSON array.', 'ACP_CONSENTMANAGER_INVALID_INTEGRATION_ENTRY' => 'Integration entry %1$s is invalid. Each entry must include a safe id, supported category, and valid script source URL.', 'EXAMPLE' => 'Example', + + // Export consent logs + 'ACP_CONSENTMANAGER_EXPORT_EXPLAIN' => 'Download a CSV file of stored consent log records. All fields are optional; leave them blank to export the full log.', + 'ACP_CONSENTMANAGER_EXPORT_FILTERS' => 'Export filters', + 'ACP_CONSENTMANAGER_EXPORT_DATE_FROM' => 'Date from', + 'ACP_CONSENTMANAGER_EXPORT_DATE_TO' => 'Date to', + 'ACP_CONSENTMANAGER_EXPORT_DATE_EXPLAIN' => 'Format: YYYY-MM-DD (UTC). Leave blank to omit this boundary.', + 'ACP_CONSENTMANAGER_EXPORT_USER_ID' => 'User ID', + 'ACP_CONSENTMANAGER_EXPORT_USER_ID_EXPLAIN' => 'Enter a registered user ID to restrict the export to that user\'s consent records. Leave blank to include all users. Note: records for guests use a session-based identifier and cannot be filtered by user ID.', + 'ACP_CONSENTMANAGER_EXPORT_VERSION' => 'Consent version', + 'ACP_CONSENTMANAGER_EXPORT_VERSION_EXPLAIN' => 'Restrict the export to a specific consent version. Leave blank for all versions.', + 'ACP_CONSENTMANAGER_EXPORT_DOWNLOAD' => 'Download CSV', + 'ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_FROM' => 'The "Date from" value is not a valid date. Use the format YYYY-MM-DD.', + 'ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_TO' => 'The "Date to" value is not a valid date. Use the format YYYY-MM-DD.', + 'ACP_CONSENTMANAGER_EXPORT_DATE_RANGE_INVALID' => '"Date from" must not be later than "Date to".', ]); diff --git a/language/en/info_acp_consentmanager.php b/language/en/info_acp_consentmanager.php index 60112a5..cf531c9 100644 --- a/language/en/info_acp_consentmanager.php +++ b/language/en/info_acp_consentmanager.php @@ -21,6 +21,8 @@ $lang = array_merge($lang, [ 'ACP_CONSENTMANAGER' => 'Consent Manager', 'ACP_CONSENTMANAGER_SETTINGS' => 'Settings', + 'ACP_CONSENTMANAGER_EXPORT' => 'Export Consent Logs', 'LOG_CONSENTMANAGER_UPDATED' => 'Updated Consent Manager settings', 'LOG_CONSENTMANAGER_REPROMPT' => 'Forced Consent Manager re-prompt by increasing the consent version', + 'LOG_CONSENTMANAGER_EXPORT' => 'Exported Consent Manager logs as CSV', ]); diff --git a/migrations/m2_export_module.php b/migrations/m2_export_module.php new file mode 100644 index 0000000..6aafa99 --- /dev/null +++ b/migrations/m2_export_module.php @@ -0,0 +1,52 @@ +table_prefix . 'modules + WHERE ' . $this->db->sql_build_array('SELECT', [ + 'module_basename' => '\phpbb\consentmanager\acp\consentmanager_module', + 'module_mode' => 'export', + ]); + + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + return (bool) $row; + } + + public function update_data() + { + return [ + ['module.add', ['acp', 'ACP_CONSENTMANAGER', [ + 'module_basename' => '\phpbb\consentmanager\acp\consentmanager_module', + 'modes' => ['export'], + ]]], + ]; + } + + public function revert_data() + { + return [ + ['module.remove', ['acp', 'ACP_CONSENTMANAGER', 'ACP_CONSENTMANAGER_EXPORT']], + ]; + } +} diff --git a/service/acp_manager.php b/service/acp_manager.php new file mode 100644 index 0000000..e6f360f --- /dev/null +++ b/service/acp_manager.php @@ -0,0 +1,224 @@ +config = $config; + $this->db = $db; + $this->log = $log; + $this->user = $user; + $this->consent_logs_table = $consent_logs_table; + } + + /** + * Parse a YYYY-MM-DD date string into a UTC timestamp. + * + * @param string $date_str Input date string + * @param bool $end_of_day When true, uses 23:59:59 instead of 00:00:00 + * + * @return int|false Timestamp on success, false if the string is empty or invalid + */ + public function parse_date_filter($date_str, $end_of_day = false) + { + if ($date_str === '') + { + return false; + } + + $dt = \DateTimeImmutable::createFromFormat('!Y-m-d', $date_str, new \DateTimeZone('UTC')); + + if ($dt === false || $dt->format('Y-m-d') !== $date_str) + { + return false; + } + + return $end_of_day + ? (int) $dt->setTime(23, 59, 59)->getTimestamp() + : (int) $dt->getTimestamp(); + } + + /** + * Write filtered consent log rows as CSV to the given file handle. + * + * Uses keyset pagination on consent_log_id to iterate rows in batches, + * avoiding memory exhaustion on large datasets. + * + * @param resource $handle Writable stream (e.g. opened on php://output) + * @param array $filters Optional: date_from, date_to, user_id, consent_version + * @param int $batch_size Rows per DB query + * + * @return void + */ + public function stream_logs_csv($handle, array $filters = [], $batch_size = 500) + { + $last_id = 0; + + do + { + $sql = 'SELECT consent_log_id, anonymized_id, consent_time, consent_version, accepted_categories' + . ' FROM ' . $this->consent_logs_table + . $this->build_filter_where($filters, $last_id) + . ' ORDER BY consent_log_id ASC'; + + $result = $this->db->sql_query_limit($sql, $batch_size); + $count = 0; + + while ($row = $this->db->sql_fetchrow($result)) + { + $count++; + $last_id = (int) $row['consent_log_id']; + $categories = json_decode($row['accepted_categories'], true); + $cat_string = is_array($categories) ? implode(',', $categories) : ''; + + fputcsv($handle, [ + $row['anonymized_id'], + gmdate('Y-m-d\TH:i:s\Z', (int) $row['consent_time']), + (int) $row['consent_version'], + $this->sanitize_csv_value($cat_string), + ]); + } + + $this->db->sql_freeresult($result); + } + while ($count === $batch_size); + } + + /** + * Compute the anonymized identifier for a given registered user ID. + * + * Mirrors the HMAC used in log_manager::log_consent() so that admins can + * filter exports by user ID without exposing raw identifiers. + * + * Note: only matches rows hashed with the current config[rand_seed]. Records + * logged before a rand_seed rotation will not be found. + * + * @param int $user_id Numeric phpBB user ID (must be > 0) + * + * @return string 64-character hex hash + */ + public function hash_user_id($user_id) + { + return hash_hmac('sha256', 'u:' . (int) $user_id, $this->config['rand_seed']); + } + + /** + * Add an admin log entry for consent settings changes. + * + * @return void + */ + public function log_admin_settings_updated() + { + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONSENTMANAGER_UPDATED'); + } + + /** + * Add an admin log entry when users are re-prompted for consent. + * + * @return void + */ + public function log_admin_reprompt() + { + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONSENTMANAGER_REPROMPT'); + } + + /** + * Add an admin log entry when consent logs are exported. + * + * @return void + */ + public function log_admin_export() + { + $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONSENTMANAGER_EXPORT'); + } + + /** + * Build a WHERE clause for consent log queries. + * + * The keyset condition (consent_log_id > last_id) is always included so + * that the caller can page through results without OFFSET. + * + * @param array $filters Filter map from parse_export_filters + * @param int $last_id Highest consent_log_id seen in the previous batch + * + * @return string SQL WHERE clause (including the leading " WHERE " keyword) + */ + protected function build_filter_where(array $filters, $last_id = 0) + { + $where = ['consent_log_id > ' . (int) $last_id]; + + if (!empty($filters['date_from'])) + { + $where[] = 'consent_time >= ' . (int) $filters['date_from']; + } + + if (!empty($filters['date_to'])) + { + $where[] = 'consent_time <= ' . (int) $filters['date_to']; + } + + if (!empty($filters['user_id'])) + { + $anonymized = $this->hash_user_id((int) $filters['user_id']); + $where[] = "anonymized_id = '" . $this->db->sql_escape($anonymized) . "'"; + } + + if (!empty($filters['consent_version'])) + { + $where[] = 'consent_version = ' . (int) $filters['consent_version']; + } + + return ' WHERE ' . implode(' AND ', $where); + } + + protected function sanitize_csv_value($value) + { + // Prevent spreadsheet formula injection (CSV injection). + // Excel/LibreOffice treat cells starting with =, +, -, @, or \t as formulas. + if ($value !== '' && strpos('=+-@' . "\t", $value[0]) !== false) + { + return "\t" . $value; + } + + return $value; + } +} diff --git a/service/log_manager.php b/service/log_manager.php index e8481f6..b0033c7 100644 --- a/service/log_manager.php +++ b/service/log_manager.php @@ -12,7 +12,6 @@ use phpbb\config\config; use phpbb\db\driver\driver_interface; -use phpbb\log\log as phpbb_log; use phpbb\user; class log_manager @@ -23,9 +22,6 @@ class log_manager /** @var driver_interface */ protected $db; - /** @var phpbb_log */ - protected $log; - /** @var user */ protected $user; @@ -37,15 +33,13 @@ class log_manager * * @param config $config Config service * @param driver_interface $db Database connection - * @param phpbb_log $log phpBB log service * @param user $user Current user * @param string $consent_logs_table Consent log table name */ - public function __construct(config $config, driver_interface $db, phpbb_log $log, user $user, $consent_logs_table) + public function __construct(config $config, driver_interface $db, user $user, $consent_logs_table) { $this->config = $config; $this->db = $db; - $this->log = $log; $this->user = $user; $this->consent_logs_table = $consent_logs_table; } @@ -71,26 +65,6 @@ public function log_consent(array $categories, $version) $this->db->sql_query($sql); } - /** - * Add an admin log entry for consent settings changes. - * - * @return void - */ - public function log_admin_settings_updated() - { - $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONSENTMANAGER_UPDATED'); - } - - /** - * Add an admin log entry when users are re-prompted for consent. - * - * @return void - */ - public function log_admin_reprompt() - { - $this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONSENTMANAGER_REPROMPT'); - } - /** * Build an anonymized identifier for the current user or session. * diff --git a/tests/acp/acp_module_test.php b/tests/acp/acp_module_test.php index a2d2902..957b8f2 100644 --- a/tests/acp/acp_module_test.php +++ b/tests/acp/acp_module_test.php @@ -59,6 +59,11 @@ public function test_module_info() 'auth' => 'ext_phpbb/consentmanager && acl_a_board', 'cat' => ['ACP_CONSENTMANAGER'] ], + 'export' => [ + 'title' => 'ACP_CONSENTMANAGER_EXPORT', + 'auth' => 'ext_phpbb/consentmanager && acl_a_board', + 'cat' => ['ACP_CONSENTMANAGER'] + ], ], ], ], $this->module_manager->get_module_infos('acp', 'consentmanager_module')); @@ -118,4 +123,33 @@ public function test_main_module() $p_master->module_ary[0]['url_extra'] = ''; $p_master->load('acp', '\phpbb\consentmanager\acp\consentmanager_module'); } + + public function test_main_module_export_mode() + { + global $phpbb_container; + + $phpbb_container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface') + ->disableOriginalConstructor() + ->getMock(); + $acp_controller = $this->getMockBuilder('\phpbb\consentmanager\controller\acp_controller') + ->disableOriginalConstructor() + ->getMock(); + + $phpbb_container + ->expects(self::once()) + ->method('get') + ->with('phpbb.consentmanager.controller.acp') + ->willReturn($acp_controller); + + $acp_controller + ->expects(self::once()) + ->method('handle_export'); + + $module = new \phpbb\consentmanager\acp\consentmanager_module(); + $module->u_action = 'adm.php?i=test&mode=export'; + $module->main('', 'export'); + + self::assertSame('consentmanager_acp_export', $module->tpl_name); + self::assertSame('ACP_CONSENTMANAGER_EXPORT', $module->page_title); + } } diff --git a/tests/controller/acp_controller_test.php b/tests/controller/acp_controller_test.php index edf7b0e..c148f43 100644 --- a/tests/controller/acp_controller_test.php +++ b/tests/controller/acp_controller_test.php @@ -30,6 +30,16 @@ protected function setUp(): void global $phpbb_root_path, $phpEx; $lang_loader = new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx); + $lang_loader->set_extension_manager(new \phpbb_mock_extension_manager( + $phpbb_root_path, + array( + 'phpbb/consentmanager' => array( + 'ext_name' => 'phpbb/consentmanager', + 'ext_active' => '1', + 'ext_path' => 'ext/phpbb/consentmanager/', + ), + ) + )); $this->language = new \phpbb\language\language($lang_loader); $this->language->add_lang('common'); $this->language->add_lang('acp_consentmanager', 'phpbb/consentmanager'); @@ -64,11 +74,11 @@ public function test_handle_assigns_existing_template_data() 'CONSENTMANAGER_VERSION' => 1, )); - $log_manager = $this->createMock('\phpbb\consentmanager\service\log_manager'); + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); $controller = new \phpbb\consentmanager\controller\acp_controller( $this->language, $consent_manager, - $log_manager, + $acp_manager, $request, $template ); @@ -118,14 +128,14 @@ public function test_handle_submit_validation_errors_reassigns_form_data() 'CONSENTMANAGER_VERSION' => 3, )); - $log_manager = $this->createMock('\phpbb\consentmanager\service\log_manager'); - $log_manager->expects(self::never()) + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + $acp_manager->expects(self::never()) ->method('log_admin_settings_updated'); $controller = new \phpbb\consentmanager\controller\acp_controller( $this->language, $consent_manager, - $log_manager, + $acp_manager, $request, $template ); @@ -155,14 +165,14 @@ public function test_handle_submit_success_logs_and_triggers_success_notice() ->method('save_acp_settings') ->willReturn(true); - $log_manager = $this->createMock('\phpbb\consentmanager\service\log_manager'); - $log_manager->expects(self::once()) + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + $acp_manager->expects(self::once()) ->method('log_admin_settings_updated'); $controller = new \phpbb\consentmanager\controller\acp_controller( $this->language, $consent_manager, - $log_manager, + $acp_manager, $request, $template ); @@ -184,14 +194,14 @@ public function test_handle_reset_consent_logs_and_triggers_success_notice() $consent_manager->expects(self::once()) ->method('reset_consent_version'); - $log_manager = $this->createMock('\phpbb\consentmanager\service\log_manager'); - $log_manager->expects(self::once()) + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + $acp_manager->expects(self::once()) ->method('log_admin_reprompt'); $controller = new \phpbb\consentmanager\controller\acp_controller( $this->language, $consent_manager, - $log_manager, + $acp_manager, $request, $template ); @@ -213,12 +223,12 @@ public function test_handle_rejects_invalid_form_key() $consent_manager->expects(self::never()) ->method('save_acp_settings'); - $log_manager = $this->createMock('\phpbb\consentmanager\service\log_manager'); + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); $controller = new \phpbb\consentmanager\controller\acp_controller( $this->language, $consent_manager, - $log_manager, + $acp_manager, $request, $template ); @@ -226,6 +236,231 @@ public function test_handle_rejects_invalid_form_key() $controller->handle(); } + public function test_handle_export_shows_empty_form() + { + $request = $this->create_request_mock(); + $template = $this->createMock('\phpbb\template\template'); + $template->expects(self::once()) + ->method('assign_vars') + ->with(array( + 'S_ERROR' => false, + 'ERROR_MSG' => '', + 'EXPORT_DATE_FROM' => '', + 'EXPORT_DATE_TO' => '', + 'EXPORT_USER_ID' => 0, + 'EXPORT_CONSENT_VER' => 0, + 'U_ACTION' => 'adm.php?i=test&mode=export', + )); + + $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $consent_manager, + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test&mode=export'); + $controller->handle_export(); + } + + public function test_handle_export_rejects_invalid_form_key() + { + self::$valid_form = false; + + $request = $this->create_request_mock(array('download_csv' => 1)); + $template = $this->createMock('\phpbb\template\template'); + $this->setExpectedTriggerError(E_USER_WARNING, $this->language->lang('FORM_INVALID')); + + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test&mode=export'); + $controller->handle_export(); + } + + public function test_handle_export_invalid_date_from_shows_error() + { + self::$valid_form = true; + + $request = $this->create_request_mock(array( + 'download_csv' => 1, + 'export_date_from' => 'not-a-date', + 'export_date_to' => '', + 'export_user_id' => 0, + 'export_consent_version' => 0, + )); + $template = $this->createMock('\phpbb\template\template'); + $template->expects(self::once()) + ->method('assign_vars') + ->with(self::callback(function ($vars) { + return $vars['S_ERROR'] === true + && strpos($vars['ERROR_MSG'], 'Date from') !== false; + })); + + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + $acp_manager->method('parse_date_filter')->willReturn(false); + $acp_manager->expects(self::never())->method('stream_logs_csv'); + $acp_manager->expects(self::never())->method('log_admin_export'); + + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test&mode=export'); + $controller->handle_export(); + } + + public function test_handle_export_invalid_date_to_shows_error() + { + self::$valid_form = true; + + $request = $this->create_request_mock(array( + 'download_csv' => 1, + 'export_date_from' => '', + 'export_date_to' => '2024-13-01', + 'export_user_id' => 0, + 'export_consent_version' => 0, + )); + $template = $this->createMock('\phpbb\template\template'); + $template->expects(self::once()) + ->method('assign_vars') + ->with(self::callback(function ($vars) { + return $vars['S_ERROR'] === true + && strpos($vars['ERROR_MSG'], 'Date to') !== false; + })); + + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + // First call (date_from, empty string) returns false; second call (date_to, invalid) returns false + $acp_manager->method('parse_date_filter')->willReturn(false); + $acp_manager->expects(self::never())->method('stream_logs_csv'); + $acp_manager->expects(self::never())->method('log_admin_export'); + + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test&mode=export'); + $controller->handle_export(); + } + + public function test_handle_export_reversed_date_range_shows_error() + { + self::$valid_form = true; + + $request = $this->create_request_mock(array( + 'download_csv' => 1, + 'export_date_from' => '2024-12-31', + 'export_date_to' => '2024-01-01', + 'export_user_id' => 0, + 'export_consent_version' => 0, + )); + $template = $this->createMock('\phpbb\template\template'); + $template->expects(self::once()) + ->method('assign_vars') + ->with(self::callback(function ($vars) { + return $vars['S_ERROR'] === true + && strpos($vars['ERROR_MSG'], '"Date from"') !== false; + })); + + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + // date_from=2024-12-31 → large timestamp, date_to=2024-01-01 → small timestamp → range error + $acp_manager->method('parse_date_filter') + ->willReturnOnConsecutiveCalls(1735603200, 1704067200); + $acp_manager->expects(self::never())->method('stream_logs_csv'); + $acp_manager->expects(self::never())->method('log_admin_export'); + + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test&mode=export'); + $controller->handle_export(); + } + + public function test_handle_reset_consent_rejects_invalid_form_key() + { + self::$valid_form = false; + + $request = $this->create_request_mock(array( + 'reset_consent' => 1, + )); + $template = $this->createMock('\phpbb\template\template'); + $this->setExpectedTriggerError(E_USER_WARNING, $this->language->lang('FORM_INVALID')); + + $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); + $consent_manager->expects(self::never()) + ->method('reset_consent_version'); + + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + $acp_manager->expects(self::never()) + ->method('log_admin_reprompt'); + + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $consent_manager, + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test'); + $controller->handle(); + } + + public function test_handle_export_success_logs_and_passes_filters_to_download() + { + self::$valid_form = true; + + $request = $this->create_request_mock(array( + 'download_csv' => 1, + 'export_date_from' => '2024-01-01', + 'export_date_to' => '2024-12-31', + 'export_user_id' => 42, + 'export_consent_version' => 2, + )); + $template = $this->createMock('\phpbb\template\template'); + + $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + $acp_manager->method('parse_date_filter') + ->willReturnOnConsecutiveCalls(1704067200, 1735689599); // 2024-01-01, 2024-12-31 23:59:59 + $acp_manager->expects(self::once())->method('log_admin_export'); + $acp_manager->expects(self::never())->method('stream_logs_csv'); // called inside send_csv_download, which is intercepted + + $controller = new \phpbb\consentmanager\tests\controller\testable_acp_controller( + $this->language, + $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), + $acp_manager, + $request, + $template + ); + $controller->set_page_url('adm.php?i=test&mode=export'); + $controller->handle_export(); + + self::assertSame(array( + 'date_from' => 1704067200, + 'date_to' => 1735689599, + 'user_id' => 42, + 'consent_version' => 2, + ), $controller->captured_filters); + } + protected function create_request_mock(array $values = array(), array $raw_values = array()) { $request = $this->getMockBuilder('\phpbb\request\request') @@ -272,6 +507,20 @@ protected function create_request_mock(array $values = array(), array $raw_value } } +namespace phpbb\consentmanager\tests\controller; + +class testable_acp_controller extends \phpbb\consentmanager\controller\acp_controller +{ + /** @var array|null Filters captured from the last send_csv_download call */ + public $captured_filters; + + protected function send_csv_download(array $filters) + { + $this->captured_filters = $filters; + // Do not stream or exit — just record the filters for assertions + } +} + namespace phpbb\consentmanager\controller; function add_form_key() diff --git a/tests/service/acp_manager_test.php b/tests/service/acp_manager_test.php new file mode 100644 index 0000000..1b1b4eb --- /dev/null +++ b/tests/service/acp_manager_test.php @@ -0,0 +1,347 @@ +language = new \phpbb\language\language($lang_loader); + + $db = $this->new_dbal(); + $db->sql_query('DELETE FROM phpbb_consentmanager_logs'); + $db->sql_close(); + } + + public function getDataSet() + { + return new \PHPUnit\DbUnit\DataSet\DefaultDataSet(); + } + + public function test_log_admin_settings_updated_delegates_to_phpbb_log() + { + $log = $this->getMockBuilder('\phpbb\log\log') + ->disableOriginalConstructor() + ->setMethods(array('add')) + ->getMock(); + $log->expects(self::once()) + ->method('add') + ->with('admin', 7, '127.0.0.1', 'LOG_CONSENTMANAGER_UPDATED'); + + $manager = $this->create_manager(7, 'admin-session', $log); + $manager->log_admin_settings_updated(); + } + + public function test_log_admin_reprompt_delegates_to_phpbb_log() + { + $log = $this->getMockBuilder('\phpbb\log\log') + ->disableOriginalConstructor() + ->setMethods(array('add')) + ->getMock(); + $log->expects(self::once()) + ->method('add') + ->with('admin', 7, '127.0.0.1', 'LOG_CONSENTMANAGER_REPROMPT'); + + $manager = $this->create_manager(7, 'admin-session', $log); + $manager->log_admin_reprompt(); + } + + public function test_log_admin_export_delegates_to_phpbb_log() + { + $log = $this->getMockBuilder('\phpbb\log\log') + ->disableOriginalConstructor() + ->setMethods(array('add')) + ->getMock(); + $log->expects(self::once()) + ->method('add') + ->with('admin', 7, '127.0.0.1', 'LOG_CONSENTMANAGER_EXPORT'); + + $manager = $this->create_manager(7, 'admin-session', $log); + $manager->log_admin_export(); + } + + public function test_hash_user_id_returns_hmac_of_user_prefix() + { + $manager = $this->create_manager(1, 'session'); + $expected = hash_hmac('sha256', 'u:42', 'random-seed'); + + self::assertSame($expected, $manager->hash_user_id(42)); + } + + public function test_hash_user_id_is_consistent() + { + $manager = $this->create_manager(1, 'session'); + + self::assertSame($manager->hash_user_id(99), $manager->hash_user_id(99)); + self::assertNotSame($manager->hash_user_id(1), $manager->hash_user_id(2)); + } + + public function test_stream_logs_csv_empty_table_writes_no_rows() + { + $manager = $this->create_manager(1, 'session'); + + $handle = fopen('php://memory', 'w+'); + $manager->stream_logs_csv($handle, []); + rewind($handle); + $content = stream_get_contents($handle); + fclose($handle); + + self::assertSame('', $content); + } + + public function test_stream_logs_csv_writes_all_rows_unfiltered() + { + $log_manager_a = $this->create_log_manager(10, 'session-a'); + $log_manager_a->log_consent(array('necessary', 'analytics'), 2); + + $log_manager_b = $this->create_log_manager(20, 'session-b'); + $log_manager_b->log_consent(array('necessary'), 2); + + $handle = fopen('php://memory', 'w+'); + $this->create_manager(1, 'session')->stream_logs_csv($handle, []); + rewind($handle); + $rows = array_filter(explode("\n", stream_get_contents($handle))); + fclose($handle); + + self::assertCount(2, $rows); + } + + public function test_stream_logs_csv_filters_by_consent_version() + { + $log_manager = $this->create_log_manager(10, 'session'); + $log_manager->log_consent(array('necessary'), 1); + $log_manager->log_consent(array('necessary', 'analytics'), 2); + $log_manager->log_consent(array('necessary'), 1); + + $handle = fopen('php://memory', 'w+'); + $this->create_manager(1, 'session')->stream_logs_csv($handle, array('consent_version' => 1)); + rewind($handle); + $rows = array_filter(explode("\n", stream_get_contents($handle))); + fclose($handle); + + self::assertCount(2, $rows); + foreach ($rows as $row) + { + self::assertStringContainsString(',1,', $row); + } + } + + public function test_stream_logs_csv_filters_by_date_range() + { + $db = $this->new_dbal(); + $now = time(); + $past = $now - 7200; // 2 hours ago + + $db->sql_query('INSERT INTO phpbb_consentmanager_logs + (anonymized_id, consent_version, accepted_categories, consent_time) + VALUES + (\'' . $db->sql_escape('hash-old') . '\', 1, \'["necessary"]\', ' . $past . '), + (\'' . $db->sql_escape('hash-new') . '\', 1, \'["necessary","analytics"]\', ' . $now . ')'); + $db->sql_close(); + + $handle = fopen('php://memory', 'w+'); + $this->create_manager(1, 'session')->stream_logs_csv($handle, array( + 'date_from' => $now - 3600, // 1 hour ago + 'date_to' => $now + 3600, + )); + rewind($handle); + $rows = array_filter(explode("\n", stream_get_contents($handle))); + fclose($handle); + + self::assertCount(1, $rows); + self::assertStringContainsString('hash-new', reset($rows)); + } + + public function test_stream_logs_csv_filters_by_user_id() + { + $manager_target = $this->create_log_manager(42, 'any-session'); + $manager_target->log_consent(array('necessary'), 1); + + $manager_other = $this->create_log_manager(99, 'other-session'); + $manager_other->log_consent(array('necessary', 'analytics'), 1); + + $reader = $this->create_manager(1, 'session'); + + $handle = fopen('php://memory', 'w+'); + $reader->stream_logs_csv($handle, array('user_id' => 42)); + rewind($handle); + $rows = array_filter(explode("\n", stream_get_contents($handle))); + fclose($handle); + + self::assertCount(1, $rows); + + $expected_hash = hash_hmac('sha256', 'u:42', 'random-seed'); + self::assertStringContainsString($expected_hash, reset($rows)); + } + + public function test_stream_logs_csv_row_format_is_correct() + { + $log_manager = $this->create_log_manager(5, 'session'); + $log_manager->log_consent(array('necessary', 'analytics'), 3); + + $handle = fopen('php://memory', 'w+'); + $this->create_manager(1, 'session')->stream_logs_csv($handle, []); + rewind($handle); + $content = stream_get_contents($handle); + fclose($handle); + + $row = str_getcsv(trim($content)); + self::assertCount(4, $row); + + // anonymized_id: 64-char hex + self::assertRegExp('/^[0-9a-f]{64}$/', $row[0]); + + // timestamp: ISO 8601 UTC + self::assertRegExp('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $row[1]); + + // consent_version + self::assertSame('3', $row[2]); + + // categories as comma-separated string + self::assertSame('necessary,analytics', $row[3]); + } + + public function test_stream_logs_csv_batch_pagination_retrieves_all_rows() + { + $log_manager = $this->create_log_manager(1, 'session'); + + for ($i = 0; $i < 5; $i++) + { + $log_manager->log_consent(array('necessary'), 1); + } + + $handle = fopen('php://memory', 'w+'); + // Use a batch size of 2 to exercise the pagination loop + $this->create_manager(1, 'session')->stream_logs_csv($handle, [], 2); + rewind($handle); + $rows = array_filter(explode("\n", stream_get_contents($handle))); + fclose($handle); + + self::assertCount(5, $rows); + } + + public function test_stream_logs_csv_sanitizes_formula_injection_in_categories() + { + $db = $this->new_dbal(); + // Insert a row whose accepted_categories begins with '=' — a formula injection attempt + $db->sql_query('INSERT INTO phpbb_consentmanager_logs + (anonymized_id, consent_version, accepted_categories, consent_time) + VALUES (\'hash-x\', 1, \'["=DANGEROUS()"]\', ' . time() . ')'); + $db->sql_close(); + + $handle = fopen('php://memory', 'w+'); + $this->create_manager(1, 'session')->stream_logs_csv($handle, []); + rewind($handle); + $row = str_getcsv(trim(stream_get_contents($handle))); + fclose($handle); + + // categories cell must be prefixed with a tab to defuse the formula + self::assertStringStartsWith("\t", $row[3]); + self::assertStringContainsString('=DANGEROUS()', $row[3]); + } + + public function test_parse_date_filter_returns_false_for_empty_string() + { + $manager = $this->create_manager(1, 'session'); + self::assertFalse($manager->parse_date_filter('')); + } + + public function test_parse_date_filter_returns_false_for_invalid_date() + { + $manager = $this->create_manager(1, 'session'); + self::assertFalse($manager->parse_date_filter('not-a-date')); + self::assertFalse($manager->parse_date_filter('2024-13-01')); + self::assertFalse($manager->parse_date_filter('2024-02-31')); + } + + public function test_parse_date_filter_returns_start_of_day_timestamp() + { + $manager = $this->create_manager(1, 'session'); + $ts = $manager->parse_date_filter('2024-06-15'); + self::assertSame( + \DateTimeImmutable::createFromFormat('!Y-m-d', '2024-06-15', new \DateTimeZone('UTC'))->getTimestamp(), + $ts + ); + } + + public function test_parse_date_filter_returns_end_of_day_timestamp_when_flag_set() + { + $manager = $this->create_manager(1, 'session'); + $start = $manager->parse_date_filter('2024-06-15'); + $end = $manager->parse_date_filter('2024-06-15', true); + self::assertSame(86399, $end - $start); // 23h 59m 59s difference + } + + protected function create_manager($user_id, $session_id, $log = null) + { + $config = new \phpbb\config\config(array( + 'rand_seed' => 'random-seed', + )); + $db = $this->new_dbal(); + + if ($log === null) + { + $log = $this->getMockBuilder('\phpbb\log\log') + ->disableOriginalConstructor() + ->getMock(); + } + + $user = new \phpbb\user($this->language, '\phpbb\datetime'); + $user->data = array( + 'user_id' => $user_id, + ); + $user->session_id = $session_id; + $user->ip = '127.0.0.1'; + + return new \phpbb\consentmanager\service\acp_manager( + $config, + $db, + $log, + $user, + 'phpbb_consentmanager_logs' + ); + } + + protected function create_log_manager($user_id, $session_id) + { + $config = new \phpbb\config\config(array( + 'rand_seed' => 'random-seed', + )); + $db = $this->new_dbal(); + + $user = new \phpbb\user($this->language, '\phpbb\datetime'); + $user->data = array( + 'user_id' => $user_id, + ); + $user->session_id = $session_id; + $user->ip = '127.0.0.1'; + + return new \phpbb\consentmanager\service\log_manager( + $config, + $db, + $user, + 'phpbb_consentmanager_logs' + ); + } +} diff --git a/tests/service/log_manager_test.php b/tests/service/log_manager_test.php index 0723fb2..a0762da 100644 --- a/tests/service/log_manager_test.php +++ b/tests/service/log_manager_test.php @@ -69,48 +69,13 @@ public function test_log_consent_uses_session_identifier_for_guests() FROM phpbb_consentmanager_logs'); } - public function test_log_admin_settings_updated_delegates_to_phpbb_log() - { - $log = $this->getMockBuilder('\phpbb\log\log') - ->disableOriginalConstructor() - ->setMethods(array('add')) - ->getMock(); - $log->expects(self::once()) - ->method('add') - ->with('admin', 7, '127.0.0.1', 'LOG_CONSENTMANAGER_UPDATED'); - - $manager = $this->create_manager(7, 'admin-session', $log); - $manager->log_admin_settings_updated(); - } - - public function test_log_admin_reprompt_delegates_to_phpbb_log() - { - $log = $this->getMockBuilder('\phpbb\log\log') - ->disableOriginalConstructor() - ->setMethods(array('add')) - ->getMock(); - $log->expects(self::once()) - ->method('add') - ->with('admin', 7, '127.0.0.1', 'LOG_CONSENTMANAGER_REPROMPT'); - - $manager = $this->create_manager(7, 'admin-session', $log); - $manager->log_admin_reprompt(); - } - - protected function create_manager($user_id, $session_id, $log = null) + protected function create_manager($user_id, $session_id) { $config = new \phpbb\config\config(array( 'rand_seed' => 'random-seed', )); $db = $this->new_dbal(); - if ($log === null) - { - $log = $this->getMockBuilder('\phpbb\log\log') - ->disableOriginalConstructor() - ->getMock(); - } - $user = new \phpbb\user($this->language, '\phpbb\datetime'); $user->data = array( 'user_id' => $user_id, @@ -121,7 +86,6 @@ protected function create_manager($user_id, $session_id, $log = null) return new \phpbb\consentmanager\service\log_manager( $config, $db, - $log, $user, 'phpbb_consentmanager_logs' ); From b3d793f52e683685b398847a6c1b80060dce7487 Mon Sep 17 00:00:00 2001 From: Matt Friedman Date: Thu, 30 Apr 2026 08:36:19 -0700 Subject: [PATCH 2/2] Refactor code --- language/en/acp_consentmanager.php | 6 +- migrations/m1_initial.php | 3 +- migrations/m2_export_module.php | 52 --- tests/acp/acp_module_test.php | 102 +++--- tests/controller/acp_controller_test.php | 447 ++++++++--------------- 5 files changed, 209 insertions(+), 401 deletions(-) delete mode 100644 migrations/m2_export_module.php diff --git a/language/en/acp_consentmanager.php b/language/en/acp_consentmanager.php index 3c20257..b21eef1 100644 --- a/language/en/acp_consentmanager.php +++ b/language/en/acp_consentmanager.php @@ -40,13 +40,13 @@ 'ACP_CONSENTMANAGER_EXPORT_FILTERS' => 'Export filters', 'ACP_CONSENTMANAGER_EXPORT_DATE_FROM' => 'Date from', 'ACP_CONSENTMANAGER_EXPORT_DATE_TO' => 'Date to', - 'ACP_CONSENTMANAGER_EXPORT_DATE_EXPLAIN' => 'Format: YYYY-MM-DD (UTC). Leave blank to omit this boundary.', + 'ACP_CONSENTMANAGER_EXPORT_DATE_EXPLAIN' => 'Use the browser date picker when available. If you cannot pick a date, enter it in YYYY-MM-DD format. Dates are interpreted in UTC. Leave blank to omit this boundary.', 'ACP_CONSENTMANAGER_EXPORT_USER_ID' => 'User ID', 'ACP_CONSENTMANAGER_EXPORT_USER_ID_EXPLAIN' => 'Enter a registered user ID to restrict the export to that user\'s consent records. Leave blank to include all users. Note: records for guests use a session-based identifier and cannot be filtered by user ID.', 'ACP_CONSENTMANAGER_EXPORT_VERSION' => 'Consent version', 'ACP_CONSENTMANAGER_EXPORT_VERSION_EXPLAIN' => 'Restrict the export to a specific consent version. Leave blank for all versions.', 'ACP_CONSENTMANAGER_EXPORT_DOWNLOAD' => 'Download CSV', - 'ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_FROM' => 'The "Date from" value is not a valid date. Use the format YYYY-MM-DD.', - 'ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_TO' => 'The "Date to" value is not a valid date. Use the format YYYY-MM-DD.', + 'ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_FROM' => 'The "Date from" value is not a valid date. Use the browser date picker when available, or enter the date in YYYY-MM-DD format.', + 'ACP_CONSENTMANAGER_EXPORT_INVALID_DATE_TO' => 'The "Date to" value is not a valid date. Use the browser date picker when available, or enter the date in YYYY-MM-DD format.', 'ACP_CONSENTMANAGER_EXPORT_DATE_RANGE_INVALID' => '"Date from" must not be later than "Date to".', ]); diff --git a/migrations/m1_initial.php b/migrations/m1_initial.php index 959f517..835dcb1 100644 --- a/migrations/m1_initial.php +++ b/migrations/m1_initial.php @@ -63,7 +63,7 @@ public function update_data() ['module.add', ['acp', 'ACP_CAT_DOT_MODS', 'ACP_CONSENTMANAGER']], ['module.add', ['acp', 'ACP_CONSENTMANAGER', [ 'module_basename' => '\phpbb\consentmanager\acp\consentmanager_module', - 'modes' => ['settings'], + 'modes' => ['settings', 'export'], ]]], ]; } @@ -75,6 +75,7 @@ public function revert_data() ['config.remove', ['consentmanager_marketing_enabled']], ['config.remove', ['consentmanager_consent_version']], ['config_text.remove', ['consentmanager_integrations']], + ['module.remove', ['acp', 'ACP_CONSENTMANAGER', 'ACP_CONSENTMANAGER_EXPORT']], ['module.remove', ['acp', 'ACP_CONSENTMANAGER', 'ACP_CONSENTMANAGER_SETTINGS']], ['module.remove', ['acp', 'ACP_CAT_DOT_MODS', 'ACP_CONSENTMANAGER']], ]; diff --git a/migrations/m2_export_module.php b/migrations/m2_export_module.php deleted file mode 100644 index 6aafa99..0000000 --- a/migrations/m2_export_module.php +++ /dev/null @@ -1,52 +0,0 @@ -table_prefix . 'modules - WHERE ' . $this->db->sql_build_array('SELECT', [ - 'module_basename' => '\phpbb\consentmanager\acp\consentmanager_module', - 'module_mode' => 'export', - ]); - - $result = $this->db->sql_query($sql); - $row = $this->db->sql_fetchrow($result); - $this->db->sql_freeresult($result); - - return (bool) $row; - } - - public function update_data() - { - return [ - ['module.add', ['acp', 'ACP_CONSENTMANAGER', [ - 'module_basename' => '\phpbb\consentmanager\acp\consentmanager_module', - 'modes' => ['export'], - ]]], - ]; - } - - public function revert_data() - { - return [ - ['module.remove', ['acp', 'ACP_CONSENTMANAGER', 'ACP_CONSENTMANAGER_EXPORT']], - ]; - } -} diff --git a/tests/acp/acp_module_test.php b/tests/acp/acp_module_test.php index 957b8f2..5e4610e 100644 --- a/tests/acp/acp_module_test.php +++ b/tests/acp/acp_module_test.php @@ -20,9 +20,21 @@ class acp_module_test extends \phpbb_test_case /** @var \phpbb\module\module_manager */ protected $module_manager; + /** @var \phpbb\request\request|\PHPUnit\Framework\MockObject\MockObject */ + protected $request; + + /** @var \phpbb\template\template|\PHPUnit\Framework\MockObject\MockObject */ + protected $template; + + /** @var \phpbb\consentmanager\controller\acp_controller|\PHPUnit\Framework\MockObject\MockObject */ + protected $acp_controller; + + /** @var \Symfony\Component\DependencyInjection\ContainerInterface|\PHPUnit\Framework\MockObject\MockObject */ + protected $container; + protected function setUp(): void { - global $phpbb_dispatcher, $phpbb_extension_manager, $phpbb_root_path, $phpEx; + global $phpbb_dispatcher, $phpbb_extension_manager, $phpbb_root_path, $phpEx, $phpbb_container, $request, $template; $this->extension_manager = new \phpbb_mock_extension_manager( $phpbb_root_path, @@ -37,7 +49,7 @@ protected function setUp(): void $this->module_manager = new \phpbb\module\module_manager( new \phpbb\cache\driver\dummy(), - $this->getMockBuilder('\phpbb\db\driver\driver_interface')->getMock(), + $this->createMock('\phpbb\db\driver\driver_interface'), $this->extension_manager, MODULES_TABLE, $phpbb_root_path, @@ -45,6 +57,20 @@ protected function setUp(): void ); $phpbb_dispatcher = new \phpbb_mock_event_dispatcher(); + + if (!defined('IN_ADMIN')) + { + define('IN_ADMIN', true); + } + + $this->request = $this->createMock('\phpbb\request\request'); + $this->template = $this->createMock('\phpbb\template\template'); + $this->acp_controller = $this->createMock('\phpbb\consentmanager\controller\acp_controller'); + $this->container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface'); + + $phpbb_container = $this->container; + $request = $this->request; + $template = $this->template; } public function test_module_info() @@ -88,68 +114,42 @@ public function test_module_auth($module_auth, $expected) public function test_main_module() { - global $phpbb_container, $request, $template; - - if (!defined('IN_ADMIN')) - { - define('IN_ADMIN', true); - } - - $request = $this->getMockBuilder('\phpbb\request\request') - ->disableOriginalConstructor() - ->getMock(); - $template = $this->getMockBuilder('\phpbb\template\template') - ->disableOriginalConstructor() - ->getMock(); - $phpbb_container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface') - ->disableOriginalConstructor() - ->getMock(); - $acp_controller = $this->getMockBuilder('\phpbb\consentmanager\controller\acp_controller') - ->disableOriginalConstructor() - ->getMock(); - - $phpbb_container - ->expects(self::once()) - ->method('get') - ->with('phpbb.consentmanager.controller.acp') - ->willReturn($acp_controller); - - $acp_controller - ->expects(self::once()) - ->method('handle'); + $this->expect_controller_method('handle'); - $p_master = new \p_master(); - $p_master->module_ary[0]['is_duplicate'] = 0; - $p_master->module_ary[0]['url_extra'] = ''; - $p_master->load('acp', '\phpbb\consentmanager\acp\consentmanager_module'); + $this->create_p_master()->load('acp', '\phpbb\consentmanager\acp\consentmanager_module'); } public function test_main_module_export_mode() { - global $phpbb_container; + $this->expect_controller_method('handle_export'); - $phpbb_container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface') - ->disableOriginalConstructor() - ->getMock(); - $acp_controller = $this->getMockBuilder('\phpbb\consentmanager\controller\acp_controller') - ->disableOriginalConstructor() - ->getMock(); + $module = new \phpbb\consentmanager\acp\consentmanager_module(); + $module->u_action = 'adm.php?i=test&mode=export'; + $module->main('', 'export'); - $phpbb_container + self::assertSame('consentmanager_acp_export', $module->tpl_name); + self::assertSame('ACP_CONSENTMANAGER_EXPORT', $module->page_title); + } + + protected function expect_controller_method($method) + { + $this->container ->expects(self::once()) ->method('get') ->with('phpbb.consentmanager.controller.acp') - ->willReturn($acp_controller); + ->willReturn($this->acp_controller); - $acp_controller + $this->acp_controller ->expects(self::once()) - ->method('handle_export'); + ->method($method); + } - $module = new \phpbb\consentmanager\acp\consentmanager_module(); - $module->u_action = 'adm.php?i=test&mode=export'; - $module->main('', 'export'); + protected function create_p_master() + { + $p_master = new \p_master(); + $p_master->module_ary[0]['is_duplicate'] = 0; + $p_master->module_ary[0]['url_extra'] = ''; - self::assertSame('consentmanager_acp_export', $module->tpl_name); - self::assertSame('ACP_CONSENTMANAGER_EXPORT', $module->page_title); + return $p_master; } } diff --git a/tests/controller/acp_controller_test.php b/tests/controller/acp_controller_test.php index c148f43..ae10e31 100644 --- a/tests/controller/acp_controller_test.php +++ b/tests/controller/acp_controller_test.php @@ -23,6 +23,15 @@ class acp_controller_test extends \phpbb_test_case /** @var \phpbb\user */ protected $user; + /** @var \phpbb\template\template|\PHPUnit\Framework\MockObject\MockObject */ + protected $template; + + /** @var \phpbb\consentmanager\service\consent_manager_interface|\PHPUnit\Framework\MockObject\MockObject */ + protected $consent_manager; + + /** @var \phpbb\consentmanager\service\acp_manager|\PHPUnit\Framework\MockObject\MockObject */ + protected $acp_manager; + protected function setUp(): void { parent::setUp(); @@ -32,217 +41,160 @@ protected function setUp(): void $lang_loader = new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx); $lang_loader->set_extension_manager(new \phpbb_mock_extension_manager( $phpbb_root_path, - array( - 'phpbb/consentmanager' => array( + [ + 'phpbb/consentmanager' => [ 'ext_name' => 'phpbb/consentmanager', 'ext_active' => '1', 'ext_path' => 'ext/phpbb/consentmanager/', - ), - ) + ], + ] )); $this->language = new \phpbb\language\language($lang_loader); $this->language->add_lang('common'); $this->language->add_lang('acp_consentmanager', 'phpbb/consentmanager'); $this->user = new \phpbb\user($this->language, '\phpbb\datetime'); - $this->user->data = array( + $this->user->data = [ 'user_id' => 2, 'user_form_salt' => 'form-salt', - ); + ]; $this->user->session_id = 'session-id'; $this->user->lang = $this->language->get_lang_array(); + + $this->template = $this->createMock('\phpbb\template\template'); + $this->consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); + $this->acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + } + + protected function create_controller($request, $u_action = 'adm.php?i=test') + { + $controller = new \phpbb\consentmanager\controller\acp_controller( + $this->language, + $this->consent_manager, + $this->acp_manager, + $request, + $this->template + ); + $controller->set_page_url($u_action); + return $controller; } public function test_handle_assigns_existing_template_data() { - $request = $this->create_request_mock(); - $template = $this->createMock('\phpbb\template\template'); - $template->expects(self::once()) + $this->consent_manager->expects(self::once()) + ->method('get_acp_template_data') + ->willReturn([ + 'S_CONSENTMANAGER_ANALYTICS' => true, + 'CONSENTMANAGER_VERSION' => 1, + ]); + $this->template->expects(self::once()) ->method('assign_vars') - ->with(array( + ->with([ 'S_CONSENTMANAGER_ANALYTICS' => true, 'CONSENTMANAGER_VERSION' => 1, 'S_ERROR' => false, 'ERROR_MSG' => '', 'U_ACTION' => 'adm.php?i=test', - )); - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $consent_manager->expects(self::once()) - ->method('get_acp_template_data') - ->willReturn(array( - 'S_CONSENTMANAGER_ANALYTICS' => true, - 'CONSENTMANAGER_VERSION' => 1, - )); + ]); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test'); - $controller->handle(); + $this->create_controller($this->create_request_mock())->handle(); } public function test_handle_submit_validation_errors_reassigns_form_data() { self::$valid_form = true; - $request = $this->create_request_mock( - array( - 'submit' => 1, - 'consentmanager_analytics_enabled' => 1, - 'consentmanager_marketing_enabled' => 0, - ), - array( - 'consentmanager_integrations' => " invalid json \n", - ) - ); - $template = $this->createMock('\phpbb\template\template'); - $template->expects(self::once()) - ->method('assign_vars') - ->with(self::callback(function ($vars) { - return $vars['S_ERROR'] - && $vars['ERROR_MSG'] === 'Invalid integrations' - && $vars['U_ACTION'] === 'adm.php?i=test' - && isset($vars['CONSENTMANAGER_VERSION']); - })); - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $consent_manager->expects(self::once()) + $this->consent_manager->expects(self::once()) ->method('save_acp_settings') ->willReturnCallback(function (array $settings, array &$errors) { - self::assertSame(array( + self::assertSame([ 'analytics_enabled' => 1, 'marketing_enabled' => 0, 'integrations' => 'invalid json', - ), $settings); - - $errors = array('Invalid integrations'); + ], $settings); + $errors = ['Invalid integrations']; return false; }); - $consent_manager->expects(self::once()) + $this->consent_manager->expects(self::once()) ->method('get_acp_template_data') - ->willReturn(array( - 'CONSENTMANAGER_VERSION' => 3, - )); - - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $acp_manager->expects(self::never()) - ->method('log_admin_settings_updated'); + ->willReturn(['CONSENTMANAGER_VERSION' => 3]); + $this->acp_manager->expects(self::never())->method('log_admin_settings_updated'); + $this->template->expects(self::once()) + ->method('assign_vars') + ->with(self::callback(function ($vars) { + return $vars['S_ERROR'] + && $vars['ERROR_MSG'] === 'Invalid integrations' + && $vars['U_ACTION'] === 'adm.php?i=test' + && isset($vars['CONSENTMANAGER_VERSION']); + })); - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template + $request = $this->create_request_mock( + [ + 'submit' => 1, + 'consentmanager_analytics_enabled' => 1, + 'consentmanager_marketing_enabled' => 0 + ], + ['consentmanager_integrations' => " invalid json \n"] ); - $controller->set_page_url('adm.php?i=test'); - $controller->handle(); + $this->create_controller($request)->handle(); } public function test_handle_submit_success_logs_and_triggers_success_notice() { self::$valid_form = true; + $this->consent_manager->expects(self::once())->method('save_acp_settings')->willReturn(true); + $this->acp_manager->expects(self::once())->method('log_admin_settings_updated'); + $this->setExpectedTriggerError(E_USER_NOTICE, $this->language->lang('CONFIG_UPDATED')); + $request = $this->create_request_mock( - array( + [ 'submit' => 1, 'consentmanager_analytics_enabled' => 0, - 'consentmanager_marketing_enabled' => 1, - ), - array( - 'consentmanager_integrations' => '[]', - ) + 'consentmanager_marketing_enabled' => 1 + ], + ['consentmanager_integrations' => '[]'] ); - $template = $this->createMock('\phpbb\template\template'); - $this->setExpectedTriggerError(E_USER_NOTICE, $this->language->lang('CONFIG_UPDATED')); - - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $consent_manager->expects(self::once()) - ->method('save_acp_settings') - ->willReturn(true); - - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $acp_manager->expects(self::once()) - ->method('log_admin_settings_updated'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test'); - $controller->handle(); + $this->create_controller($request)->handle(); } public function test_handle_reset_consent_logs_and_triggers_success_notice() { self::$valid_form = true; - $request = $this->create_request_mock(array( - 'reset_consent' => 1, - )); - $template = $this->createMock('\phpbb\template\template'); + $this->consent_manager->expects(self::once())->method('reset_consent_version'); + $this->acp_manager->expects(self::once())->method('log_admin_reprompt'); $this->setExpectedTriggerError(E_USER_NOTICE, $this->language->lang('ACP_CONSENTMANAGER_REPROMPT_SUCCESS')); - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $consent_manager->expects(self::once()) - ->method('reset_consent_version'); - - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $acp_manager->expects(self::once()) - ->method('log_admin_reprompt'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test'); - $controller->handle(); + $this->create_controller($this->create_request_mock(['reset_consent' => 1]))->handle(); } public function test_handle_rejects_invalid_form_key() { self::$valid_form = false; - $request = $this->create_request_mock(array( - 'submit' => 1, - )); - $template = $this->createMock('\phpbb\template\template'); + $this->consent_manager->expects(self::never())->method('save_acp_settings'); $this->setExpectedTriggerError(E_USER_WARNING, $this->language->lang('FORM_INVALID')); - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $consent_manager->expects(self::never()) - ->method('save_acp_settings'); + $this->create_controller($this->create_request_mock(['submit' => 1]))->handle(); + } - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); + public function test_handle_reset_consent_rejects_invalid_form_key() + { + self::$valid_form = false; - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test'); - $controller->handle(); + $this->consent_manager->expects(self::never())->method('reset_consent_version'); + $this->acp_manager->expects(self::never())->method('log_admin_reprompt'); + $this->setExpectedTriggerError(E_USER_WARNING, $this->language->lang('FORM_INVALID')); + + $this->create_controller($this->create_request_mock(['reset_consent' => 1]))->handle(); } public function test_handle_export_shows_empty_form() { - $request = $this->create_request_mock(); - $template = $this->createMock('\phpbb\template\template'); - $template->expects(self::once()) + $this->template->expects(self::once()) ->method('assign_vars') - ->with(array( + ->with([ 'S_ERROR' => false, 'ERROR_MSG' => '', 'EXPORT_DATE_FROM' => '', @@ -250,222 +202,129 @@ public function test_handle_export_shows_empty_form() 'EXPORT_USER_ID' => 0, 'EXPORT_CONSENT_VER' => 0, 'U_ACTION' => 'adm.php?i=test&mode=export', - )); + ]); - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test&mode=export'); - $controller->handle_export(); + $this->create_controller($this->create_request_mock(), 'adm.php?i=test&mode=export')->handle_export(); } public function test_handle_export_rejects_invalid_form_key() { self::$valid_form = false; - $request = $this->create_request_mock(array('download_csv' => 1)); - $template = $this->createMock('\phpbb\template\template'); $this->setExpectedTriggerError(E_USER_WARNING, $this->language->lang('FORM_INVALID')); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test&mode=export'); - $controller->handle_export(); + $this->create_controller($this->create_request_mock(['download_csv' => 1]), 'adm.php?i=test&mode=export')->handle_export(); } public function test_handle_export_invalid_date_from_shows_error() { self::$valid_form = true; - $request = $this->create_request_mock(array( - 'download_csv' => 1, - 'export_date_from' => 'not-a-date', - 'export_date_to' => '', - 'export_user_id' => 0, - 'export_consent_version' => 0, - )); - $template = $this->createMock('\phpbb\template\template'); - $template->expects(self::once()) + $this->acp_manager->method('parse_date_filter')->willReturn(false); + $this->acp_manager->expects(self::never())->method('stream_logs_csv'); + $this->acp_manager->expects(self::never())->method('log_admin_export'); + $this->template->expects(self::once()) ->method('assign_vars') ->with(self::callback(function ($vars) { - return $vars['S_ERROR'] === true - && strpos($vars['ERROR_MSG'], 'Date from') !== false; + return $vars['S_ERROR'] === true && strpos($vars['ERROR_MSG'], 'Date from') !== false; })); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $acp_manager->method('parse_date_filter')->willReturn(false); - $acp_manager->expects(self::never())->method('stream_logs_csv'); - $acp_manager->expects(self::never())->method('log_admin_export'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test&mode=export'); - $controller->handle_export(); + $request = $this->create_request_mock([ + 'download_csv' => 1, + 'export_date_from' => 'not-a-date', + 'export_date_to' => '', + 'export_user_id' => 0, + 'export_consent_version' => 0, + ]); + $this->create_controller($request, 'adm.php?i=test&mode=export')->handle_export(); } public function test_handle_export_invalid_date_to_shows_error() { self::$valid_form = true; - $request = $this->create_request_mock(array( - 'download_csv' => 1, - 'export_date_from' => '', - 'export_date_to' => '2024-13-01', - 'export_user_id' => 0, - 'export_consent_version' => 0, - )); - $template = $this->createMock('\phpbb\template\template'); - $template->expects(self::once()) + $this->acp_manager->method('parse_date_filter')->willReturn(false); + $this->acp_manager->expects(self::never())->method('stream_logs_csv'); + $this->acp_manager->expects(self::never())->method('log_admin_export'); + $this->template->expects(self::once()) ->method('assign_vars') ->with(self::callback(function ($vars) { - return $vars['S_ERROR'] === true - && strpos($vars['ERROR_MSG'], 'Date to') !== false; + return $vars['S_ERROR'] === true && strpos($vars['ERROR_MSG'], 'Date to') !== false; })); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - // First call (date_from, empty string) returns false; second call (date_to, invalid) returns false - $acp_manager->method('parse_date_filter')->willReturn(false); - $acp_manager->expects(self::never())->method('stream_logs_csv'); - $acp_manager->expects(self::never())->method('log_admin_export'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test&mode=export'); - $controller->handle_export(); + $request = $this->create_request_mock([ + 'download_csv' => 1, + 'export_date_from' => '', + 'export_date_to' => '2024-13-01', + 'export_user_id' => 0, + 'export_consent_version' => 0, + ]); + $this->create_controller($request, 'adm.php?i=test&mode=export')->handle_export(); } public function test_handle_export_reversed_date_range_shows_error() { self::$valid_form = true; - $request = $this->create_request_mock(array( - 'download_csv' => 1, - 'export_date_from' => '2024-12-31', - 'export_date_to' => '2024-01-01', - 'export_user_id' => 0, - 'export_consent_version' => 0, - )); - $template = $this->createMock('\phpbb\template\template'); - $template->expects(self::once()) + $this->acp_manager->method('parse_date_filter') + ->willReturnOnConsecutiveCalls(1735603200, 1704067200); + $this->acp_manager->expects(self::never())->method('stream_logs_csv'); + $this->acp_manager->expects(self::never())->method('log_admin_export'); + $this->template->expects(self::once()) ->method('assign_vars') ->with(self::callback(function ($vars) { - return $vars['S_ERROR'] === true - && strpos($vars['ERROR_MSG'], '"Date from"') !== false; + return $vars['S_ERROR'] === true && strpos($vars['ERROR_MSG'], '"Date from"') !== false; })); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - // date_from=2024-12-31 → large timestamp, date_to=2024-01-01 → small timestamp → range error - $acp_manager->method('parse_date_filter') - ->willReturnOnConsecutiveCalls(1735603200, 1704067200); - $acp_manager->expects(self::never())->method('stream_logs_csv'); - $acp_manager->expects(self::never())->method('log_admin_export'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test&mode=export'); - $controller->handle_export(); - } - - public function test_handle_reset_consent_rejects_invalid_form_key() - { - self::$valid_form = false; - - $request = $this->create_request_mock(array( - 'reset_consent' => 1, - )); - $template = $this->createMock('\phpbb\template\template'); - $this->setExpectedTriggerError(E_USER_WARNING, $this->language->lang('FORM_INVALID')); - - $consent_manager = $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'); - $consent_manager->expects(self::never()) - ->method('reset_consent_version'); - - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $acp_manager->expects(self::never()) - ->method('log_admin_reprompt'); - - $controller = new \phpbb\consentmanager\controller\acp_controller( - $this->language, - $consent_manager, - $acp_manager, - $request, - $template - ); - $controller->set_page_url('adm.php?i=test'); - $controller->handle(); + $request = $this->create_request_mock([ + 'download_csv' => 1, + 'export_date_from' => '2024-12-31', + 'export_date_to' => '2024-01-01', + 'export_user_id' => 0, + 'export_consent_version' => 0, + ]); + $this->create_controller($request, 'adm.php?i=test&mode=export')->handle_export(); } public function test_handle_export_success_logs_and_passes_filters_to_download() { self::$valid_form = true; - $request = $this->create_request_mock(array( - 'download_csv' => 1, - 'export_date_from' => '2024-01-01', - 'export_date_to' => '2024-12-31', - 'export_user_id' => 42, - 'export_consent_version' => 2, - )); - $template = $this->createMock('\phpbb\template\template'); + $this->acp_manager->method('parse_date_filter') + ->willReturnOnConsecutiveCalls(1704067200, 1735689599); + $this->acp_manager->expects(self::once())->method('log_admin_export'); - $acp_manager = $this->createMock('\phpbb\consentmanager\service\acp_manager'); - $acp_manager->method('parse_date_filter') - ->willReturnOnConsecutiveCalls(1704067200, 1735689599); // 2024-01-01, 2024-12-31 23:59:59 - $acp_manager->expects(self::once())->method('log_admin_export'); - $acp_manager->expects(self::never())->method('stream_logs_csv'); // called inside send_csv_download, which is intercepted + $request = $this->create_request_mock([ + 'download_csv' => 1, + 'export_date_from' => '2024-01-01', + 'export_date_to' => '2024-12-31', + 'export_user_id' => 42, + 'export_consent_version' => 2, + ]); $controller = new \phpbb\consentmanager\tests\controller\testable_acp_controller( $this->language, - $this->createMock('\phpbb\consentmanager\service\consent_manager_interface'), - $acp_manager, + $this->consent_manager, + $this->acp_manager, $request, - $template + $this->template ); $controller->set_page_url('adm.php?i=test&mode=export'); $controller->handle_export(); - self::assertSame(array( - 'date_from' => 1704067200, - 'date_to' => 1735689599, - 'user_id' => 42, + self::assertSame([ + 'date_from' => 1704067200, + 'date_to' => 1735689599, + 'user_id' => 42, 'consent_version' => 2, - ), $controller->captured_filters); + ], $controller->captured_filters); } - protected function create_request_mock(array $values = array(), array $raw_values = array()) + protected function create_request_mock(array $values = [], array $raw_values = []) { $request = $this->getMockBuilder('\phpbb\request\request') ->disableOriginalConstructor() - ->setMethods(array('is_set_post', 'variable', 'raw_variable')) + ->setMethods(['is_set_post', 'variable', 'raw_variable']) ->getMock(); $request->method('is_set_post')