plugin = $plugin; add_action('send_headers', [$this, 'add_security_headers']); add_action('admin_init', [$this, 'register_settings']); add_action('update_option_' . self::OPTION_NAME, [$this, 'sync_htaccess']); } public function sync_htaccess() { if (isset($this->plugin->htaccess_manager)) { $this->plugin->htaccess_manager->update_htaccess(); } } public function add_security_headers() { if (headers_sent() || is_admin()) { return; } $options = get_option(self::OPTION_NAME, $this->get_default_options()); if (!is_array($options)) { return; } foreach ($options as $key => $header) { if (!empty($header['enabled']) && !empty($header['name']) && !empty($header['value'])) { if ($key === 'hsts' && !is_ssl()) { continue; } header(trim($header['name']) . ': ' . trim($header['value']), true); } if ($key === 'server_header' && !empty($header['enabled']) && $header['value'] === 'remove') { header_remove('Server'); } } } public function register_settings() { register_setting(self::OPTION_GROUP, self::OPTION_NAME, [ 'sanitize_callback' => [$this, 'sanitize_options'], 'default' => $this->get_default_options() ]); } public function get_default_options() { return [ 'referrer_policy' => ['name' => 'Referrer-Policy', 'value' => 'strict-origin-when-cross-origin', 'enabled' => true, 'description' => __('Controls referrer info sent to other sites.', 'advanced-ip-blocker')], 'permissions_policy' => ['name' => 'Permissions-Policy', 'value' => 'microphone=(), camera=(), geolocation=()', 'enabled' => true, 'description' => __('Controls which browser features can be used.', 'advanced-ip-blocker')], 'x_frame_options' => ['name' => 'X-Frame-Options', 'value' => 'SAMEORIGIN', 'enabled' => true, 'description' => __('Protects against clickjacking attacks.', 'advanced-ip-blocker')], 'x_content_type_options' => ['name' => 'X-Content-Type-Options', 'value' => 'nosniff', 'enabled' => true, 'description' => __('Prevents MIME-sniffing vulnerabilities.', 'advanced-ip-blocker')], 'hsts' => ['name' => 'Strict-Transport-Security', 'value' => 'max-age=31536000; includeSubDomains; preload', 'enabled' => true, 'description' => __('Warning: Only use if your site is fully on HTTPS.', 'advanced-ip-blocker')], 'csp' => ['name' => 'Content-Security-Policy', 'value' => "", 'enabled' => false, 'description' => __('Enforcing CSP. Build your policy using the Report-Only mode below first. Warning: Incorrect CSP can break your site.', 'advanced-ip-blocker')], 'csp_report_only' => ['name' => 'Content-Security-Policy-Report-Only', 'value' => "default-src 'self';", 'enabled' => false, 'description' => __('CSP Report-Only Mode. Use this to test your policy.', 'advanced-ip-blocker')], 'server_header' => ['name' => 'Server', 'value' => '', 'enabled' => false, 'description' => __('Modify or remove the "Server" header. To remove, set value to "remove". Obfuscates server technology.', 'advanced-ip-blocker')], 'custom_1' => ['name' => '', 'value' => '', 'enabled' => false, 'description' => __('Define a custom header. Example: X-Forwarded-For', 'advanced-ip-blocker')], 'custom_2' => ['name' => '', 'value' => '', 'enabled' => false, 'description' => __('Define another custom header.', 'advanced-ip-blocker')], 'custom_3' => ['name' => '', 'value' => '', 'enabled' => false, 'description' => __('Define a third custom header.', 'advanced-ip-blocker')], ]; } public function sanitize_options($input) { $defaults = $this->get_default_options(); $output = []; foreach ($defaults as $key => $values) { $output[$key]['description'] = $values['description']; $output[$key]['enabled'] = !empty($input[$key]['enabled']) ? 1 : 0; $output[$key]['name'] = isset($input[$key]['name']) ? sanitize_text_field($input[$key]['name']) : $values['name']; if ($key === 'csp' || $key === 'csp_report_only') { // CSP can contain various chars, stripping tags is usually enough but we must be careful. // wp_kses_post allows too much HTML. sanitize_text_field strips too much (quotes). // We will use a custom sanitization that preserves CSP syntax but removes tags. $output[$key]['value'] = isset($input[$key]['value']) ? $this->sanitize_csp($input[$key]['value']) : ''; } else { $output[$key]['value'] = isset($input[$key]['value']) ? sanitize_text_field($input[$key]['value']) : ''; } } return $output; } private function sanitize_csp($value) { // Strip tags but allow quotes, semicolons, etc. $value = wp_strip_all_tags($value); // Remove line breaks (copy/paste issue) to prevent .htaccess 500 errors $value = str_replace(["\r", "\n"], ' ', $value); // Replace double quotes with single quotes to prevent breaking .htaccess string wrapping $value = str_replace('"', "'", $value); // Compress multiple spaces into one $value = preg_replace('/\s+/', ' ', $value); return trim($value); } public function display_settings_tab() { if (!current_user_can('manage_options')) return; ?>