options_repository = $options_repository; $this->nonce_service = $nonce_service; $this->capabilities_service = $capabilities_service; $this->logger = $logger; } /** * Initialize settings page. */ public function init(): void { add_action('admin_menu', [$this, 'add_admin_menu']); add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('admin_post_adsd_update_general', [$this, 'handle_update_general']); add_action('admin_post_adsd_reset_rules', [$this, 'handle_reset_rules']); add_action('wp_ajax_adsd_update_rule', [$this, 'handle_ajax_update_rule']); add_action('wp_ajax_adsd_delete_rule', [$this, 'handle_ajax_delete_rule']); add_action('wp_ajax_adsd_toggle_rule', [$this, 'handle_ajax_toggle_rule']); add_action('wp_ajax_adsd_get_latest_log', [$this, 'handle_get_latest_log']); add_action('wp_ajax_adsd_download_latest_log', [$this, 'handle_download_latest_log']); add_action('wp_ajax_adsd_clear_all_logs', [$this, 'handle_clear_all_logs']); add_action('wp_ajax_adsd_log_js_message', [$this, 'handle_log_js_message']); } /** * Add admin menu item. */ public function add_admin_menu(): void { add_options_page( __('AdsDestroyer', 'ads-destroyer'), __('AdsDestroyer', 'ads-destroyer'), 'manage_options', $this->menu_slug, [$this, 'render_settings_page'] ); } /** * Enqueue admin scripts and styles for settings page. */ public function enqueue_admin_assets(): void { // Only load on our settings page $screen = get_current_screen(); if (!$screen || $screen->id !== 'settings_page_' . $this->menu_slug) { return; } // Enqueue admin settings CSS wp_enqueue_style( 'adsd-admin-settings-style', plugin_dir_url(\ADSD_PLUGIN_FILE) . 'build/css/admin-settings-style.min.css', [], ADSD_PLUGIN_VERSION ); // Enqueue admin settings JavaScript wp_enqueue_script( 'adsd-admin-settings', plugin_dir_url(\ADSD_PLUGIN_FILE) . 'build/js/admin-settings.js', ['jquery'], ADSD_PLUGIN_VERSION, true ); // Localize script with AJAX URL wp_localize_script('adsd-admin-settings', 'adsd_ajax', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('adsd_update_rule_ajax'), ]); } /** * Render settings page. */ public function render_settings_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'ads-destroyer')); } $opts = $this->options_repository->get_all(); // Sanitize tab parameter with allowed values $allowed_tabs = ['rules', 'settings', 'logs']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- URL parameter, not form submission $active_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'rules'; if (!in_array($active_tab, $allowed_tabs, true)) { $active_tab = 'rules'; } ?>

render_admin_notices(); ?> render_rules_tab(); ?> render_settings_tab(); ?> render_logs_tab(); ?>

' . esc_html__('All rules have been reset successfully.', 'ads-destroyer') . '

'; } ?>

render_rules_table(); ?> render_instructions(); ?>
options_repository->get_all(); // Show success message if settings were updated // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- URL parameter check, not form submission if (isset($_GET['updated']) && sanitize_text_field(wp_unslash($_GET['updated'])) === '1') { echo '

' . esc_html__('Settings saved successfully.', 'ads-destroyer') . '

'; } ?>

options_repository->get_all(); // Update UI settings $opts['ui']['show_admin_bar_button'] = !empty($_POST['show_admin_bar_button']); // Update access settings // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array is sanitized via array_map $roles_allowed = isset($_POST['roles_allowed']) ? wp_unslash($_POST['roles_allowed']) : ['administrator']; $opts['access']['roles_allowed'] = array_map('sanitize_text_field', $roles_allowed); // Update logging settings $opts['logging']['enabled'] = !empty($_POST['logging_enabled']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array is sanitized via array_map $log_levels = isset($_POST['log_levels']) ? wp_unslash($_POST['log_levels']) : ['error']; $opts['logging']['levels'] = array_map('sanitize_text_field', $log_levels); $this->options_repository->save_all($opts); // Update logger settings if ($this->logger) { if ($opts['logging']['enabled']) { $this->logger->enable(); $this->logger->set_log_levels($opts['logging']['levels']); } else { $this->logger->disable(); } } // Add admin notice add_action('admin_notices', function() { echo '

' . esc_html__('Settings saved successfully.', 'ads-destroyer') . '

'; }); wp_safe_redirect(add_query_arg(['updated' => '1'], admin_url('admin.php?page=ads-destroyer&tab=settings'))); exit; } /** * Handle get latest log AJAX request. */ public function handle_get_latest_log(): void { $nonce = sanitize_text_field(wp_unslash($_POST['_wpnonce'] ?? '')); if (!wp_verify_nonce($nonce, 'adsd_get_latest_log')) { wp_die(esc_html__('Security check failed', 'ads-destroyer')); } $latest_file = $this->logger->get_latest_log_file(); if ($latest_file) { $content = $this->logger->read_log_file($latest_file, 1000); echo esc_html($content); } else { echo esc_html__('No log files found.', 'ads-destroyer'); } wp_die(); } /** * Handle download latest log AJAX request. */ public function handle_download_latest_log(): void { $nonce = sanitize_text_field(wp_unslash($_POST['_wpnonce'] ?? '')); if (!wp_verify_nonce($nonce, 'adsd_download_latest_log')) { wp_die(esc_html__('Security check failed', 'ads-destroyer')); } $latest_file = $this->logger->get_latest_log_file(); if ($latest_file && file_exists($latest_file)) { $content = file_get_contents($latest_file); header('Content-Type: text/plain'); header('Content-Disposition: attachment; filename="ads-destroyer-latest.log"'); header('Content-Length: ' . strlen($content)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary file content echo $content; } else { wp_die(esc_html__('No log files found.', 'ads-destroyer')); } exit; } /** * Handle clear all logs AJAX request. */ public function handle_clear_all_logs(): void { $nonce = sanitize_text_field(wp_unslash($_POST['_wpnonce'] ?? '')); if (!wp_verify_nonce($nonce, 'adsd_clear_all_logs')) { wp_send_json_error(__('Security check failed', 'ads-destroyer')); } $deleted_count = $this->logger->clear_all_logs(); wp_send_json_success([ 'count' => $deleted_count, // translators: %d: Number of deleted log files 'message' => sprintf(__('Cleared %d log files.', 'ads-destroyer'), $deleted_count) ]); } /** * Handle JavaScript log message AJAX request. */ public function handle_log_js_message(): void { $nonce = sanitize_text_field(wp_unslash($_POST['_wpnonce'] ?? '')); if (!wp_verify_nonce($nonce, 'adsd_log_js_message')) { wp_die(esc_html__('Security check failed', 'ads-destroyer')); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data is validated and sanitized after decode $log_entry_json = isset($_POST['log_entry']) ? wp_unslash($_POST['log_entry']) : ''; $log_entry = json_decode($log_entry_json, true); if (!$log_entry) { wp_die(esc_html__('Invalid log entry', 'ads-destroyer')); } // Sanitize decoded JSON data recursively $log_entry = $this->sanitize_array($log_entry); // Log the JavaScript message using PHP logger $message = sprintf('[JS] %s', $log_entry['message'] ?? ''); $context = array_merge($log_entry['context'] ?? [], [ 'source' => 'javascript', 'user_agent' => $log_entry['userAgent'] ?? '', 'url' => $log_entry['url'] ?? '' ]); $this->logger->log($log_entry['level'], $message, $context); wp_die(); } /** * Render admin notices. */ private function render_admin_notices(): void { $notice_code = isset($_GET['adsd_notice']) ? sanitize_key((string) $_GET['adsd_notice']) : ''; $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : ''; if ($notice_code && $nonce && wp_verify_nonce($nonce, 'adsd_notice_' . $notice_code)) { $messages = [ 'general_ok' => __('Settings saved.', 'ads-destroyer'), 'logging_ok' => __('Settings saved.', 'ads-destroyer'), 'reset_ok' => __('All rules have been reset.', 'ads-destroyer'), 'delete_ok' => __('Rule deleted.', 'ads-destroyer'), 'update_ok' => __('Rule updated.', 'ads-destroyer'), 'toggle_ok' => __('Rule status updated.', 'ads-destroyer'), ]; $msg = $messages[$notice_code] ?? ''; if ($msg) { echo '

' . esc_html($msg) . '

'; } } } /** * Render rules table using WP_List_Table. */ private function render_rules_table(): void { $rules_table = new RulesListTable($this->options_repository); $rules_table->display(); // Add modal for editing rules $this->render_edit_modal(); } /** * Render modal for editing rules. */ private function render_edit_modal(): void { ?> options_repository->get_all(); $opts['rules'] = []; $this->options_repository->save_all($opts); if ($this->logger) { $this->logger->info('All rules reset by user: ' . wp_get_current_user()->user_login); } wp_safe_redirect(add_query_arg(['updated' => '1'], admin_url('admin.php?page=ads-destroyer&tab=rules'))); exit; } /** * Handle AJAX update rule. */ public function handle_ajax_update_rule(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in verify_nonce call below $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!$this->nonce_service->verify_nonce($nonce, 'adsd_update_rule_ajax')) { wp_die(esc_html__('Security check failed', 'ads-destroyer')); } if (!$this->capabilities_service->can_access_settings()) { wp_die(esc_html__('Insufficient permissions', 'ads-destroyer')); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above $rule_id = isset($_POST['rule_id']) ? sanitize_text_field(wp_unslash($_POST['rule_id'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified above, xpath sanitized via SanitizerService $xpath_raw = isset($_POST['xpath']) ? wp_unslash($_POST['xpath']) : ''; $xpath = SanitizerService::sanitize_xpath($xpath_raw); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above $page_info = isset($_POST['page_info']) ? sanitize_text_field(wp_unslash($_POST['page_info'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above $enabled = isset($_POST['enabled']); if (empty($rule_id) || empty($xpath)) { wp_send_json_error(['message' => __('Invalid data', 'ads-destroyer')]); } $opts = $this->options_repository->get_all(); $rules = $opts['rules'] ?? []; foreach ($rules as &$rule) { if (($rule['id'] ?? '') === $rule_id) { $rule['xpath'] = $xpath; $rule['page_info'] = $page_info; $rule['enabled'] = $enabled; break; } } $opts['rules'] = $rules; $this->options_repository->save_all($opts); $this->logger->info('Rule updated by user: ' . wp_get_current_user()->user_login . ' - Rule ID: ' . $rule_id); wp_send_json_success(['message' => __('Rule updated successfully', 'ads-destroyer')]); } /** * Handle AJAX delete rule. */ public function handle_ajax_delete_rule(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in verify_nonce call below $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!$this->nonce_service->verify_nonce($nonce, 'adsd_update_rule_ajax')) { wp_die(esc_html__('Security check failed', 'ads-destroyer')); } if (!$this->capabilities_service->can_access_settings()) { wp_die(esc_html__('Insufficient permissions', 'ads-destroyer')); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above $rule_id = isset($_POST['rule_id']) ? sanitize_text_field(wp_unslash($_POST['rule_id'])) : ''; if (empty($rule_id)) { wp_send_json_error(['message' => __('Invalid rule ID', 'ads-destroyer')]); } $opts = $this->options_repository->get_all(); $rules = $opts['rules'] ?? []; $rules = array_filter($rules, function($rule) use ($rule_id) { return ($rule['id'] ?? '') !== $rule_id; }); $opts['rules'] = array_values($rules); $this->options_repository->save_all($opts); $this->logger->info('Rule deleted by user: ' . wp_get_current_user()->user_login . ' - Rule ID: ' . $rule_id); wp_send_json_success(['message' => __('Rule deleted successfully', 'ads-destroyer')]); } /** * Handle AJAX toggle rule. */ public function handle_ajax_toggle_rule(): void { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in verify_nonce call below $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!$this->nonce_service->verify_nonce($nonce, 'adsd_update_rule_ajax')) { wp_die(esc_html__('Security check failed', 'ads-destroyer')); } if (!$this->capabilities_service->can_access_settings()) { wp_die(esc_html__('Insufficient permissions', 'ads-destroyer')); } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above $rule_id = isset($_POST['rule_id']) ? sanitize_text_field(wp_unslash($_POST['rule_id'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above $enabled = isset($_POST['enabled']) ? filter_var(wp_unslash($_POST['enabled']), FILTER_VALIDATE_BOOLEAN) : false; if (empty($rule_id)) { wp_send_json_error(['message' => __('Invalid rule ID', 'ads-destroyer')]); } $opts = $this->options_repository->get_all(); $rules = $opts['rules'] ?? []; foreach ($rules as &$rule) { if (($rule['id'] ?? '') === $rule_id) { $rule['enabled'] = $enabled; break; } } $opts['rules'] = $rules; $this->options_repository->save_all($opts); $this->logger->info('Rule toggled by user: ' . wp_get_current_user()->user_login . ' - Rule ID: ' . $rule_id . ' - Enabled: ' . ($enabled ? 'true' : 'false')); wp_send_json_success(['message' => __('Rule status updated', 'ads-destroyer')]); } /** * Recursively sanitize array data. * * @param mixed $data Data to sanitize. * @return mixed Sanitized data. */ private function sanitize_array($data) { if (is_array($data)) { return array_map([$this, 'sanitize_array'], $data); } elseif (is_string($data)) { return sanitize_text_field($data); } elseif (is_int($data)) { return absint($data); } elseif (is_float($data)) { return filter_var($data, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); } else { return $data; } } }