From 95e00ffdd924f0d67176be8c258a608dd9ca932c Mon Sep 17 00:00:00 2001 From: Alex Lion Date: Mon, 15 Sep 2025 12:55:22 -0700 Subject: [PATCH 01/18] Improve I18N Issue based on 2.5.3 --- disable-comments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disable-comments.php b/disable-comments.php index e8e4fa9..01462be 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1460,7 +1460,7 @@ public function add_site_health_info($debug_info) { ), 'disabled_post_type_count' => array( 'label' => __('Disabled Post Types Count', 'disable-comments'), - 'value' => sprintf('%d of %d', count($data['disabled_post_types']), $data['total_post_types']), + 'value' => sprintf(__('%1$d of %2$d', 'disable-comments'), count($data['disabled_post_types']), $data['total_post_types']), ), 'disabled_post_types' => array( 'label' => __('Disabled Post Types', 'disable-comments'), From acbc92f4c8996ba705727c58cadb965d468d09af Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:29:26 +0600 Subject: [PATCH 02/18] Add AI agent context, CLAUDE.md, and dev tooling exclusions - Add CLAUDE.md with project overview, architecture notes, and security summary - Add .ai/ agent context directory (security vulnerability docs excluded from git) - Exclude .ai/, .ai/security/, and CLAUDE.md from distribution via .distignore Co-Authored-By: Claude Sonnet 4.6 --- .ai/README.md | 27 ++++++++++++++++++ .distignore | 2 ++ .gitignore | 1 + CLAUDE.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 .ai/README.md create mode 100644 CLAUDE.md diff --git a/.ai/README.md b/.ai/README.md new file mode 100644 index 0000000..37b38c7 --- /dev/null +++ b/.ai/README.md @@ -0,0 +1,27 @@ +# .ai/ — Agent Context Directory + +This directory stores documentation and context generated or maintained by AI agents (Claude Code, etc.) during development work. + +## Purpose + +- **Deep-dive architecture docs** — detail too verbose for `CLAUDE.md` +- **Investigation notes** — written while exploring complex features +- **Feature context** — background and rationale for non-obvious design decisions + +This is NOT a replacement for code comments, git history, or `docs/`. It supplements `CLAUDE.md` with detail that would otherwise bloat it. + +> **Agents:** When you discover context worth preserving, write it here — not into `CLAUDE.md`. Reference the file from `CLAUDE.md` with a one-line pointer. + +## Structure + +```text +.ai/ + README.md # This file — directory overview + security/ # Vulnerability reports and fix guidance (excluded from git) +``` + +## Rules for Agents + +- **Write here, not in `CLAUDE.md`** — keep CLAUDE.md as a concise rules + pointers file +- **One file per topic** — don't append unrelated notes to an existing file +- **This directory is excluded from distribution zips** — `.distignore` covers `.ai/` recursively diff --git a/.distignore b/.distignore index 671a2c5..68cca0f 100644 --- a/.distignore +++ b/.distignore @@ -38,3 +38,5 @@ src assets/img/card *.map assets/scss +.ai/ +CLAUDE.md diff --git a/.gitignore b/.gitignore index 0932451..82c1dfc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ vendor/ .idea package-lock.json pnpm-lock.yaml +.ai/security/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1a25a4f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# Disable Comments — Plugin Development Guide + +WordPress plugin by WPDeveloper. Allows administrators to globally disable comments by post type, with multisite network support. + +- **WordPress.org:** +- **Current version:** 2.6.2 +- **Main class:** `Disable_Comments` (singleton) in `disable-comments.php` + +--- + +## Project Structure + +```text +disable-comments.php Main plugin file (~2000 lines), single class +includes/ + cli.php WP-CLI command definitions + class-plugin-usage-tracker.php +views/ + settings.php Main settings page shell + comments.php Tools/delete page shell + partials/ + _disable.php Disable-comments form (main settings form) + _delete.php Delete comments form + _sites.php Multisite sub-site list + _menu.php / _footer.php / _sidebar.php +assets/ + js/disable-comments-settings-scripts.js Settings page JS (role exclusion UI, AJAX calls) + js/disable-comments.js + css/ scss/ +tests/ + test-plugin.php PHPUnit tests (Brain/Monkey mocking) + bootstrap.php +``` + +--- + +## Key AJAX Handlers + +All three AJAX handlers are registered in `__construct()` (~line 49): + +| Action | Handler | Line | +| ------ | ------- | ---- | +| `disable_comments_save_settings` | `disable_comments_settings()` | ~1217 | +| `disable_comments_delete_comments` | `delete_comments_settings()` | ~1324 | +| `get_sub_sites` | `get_sub_sites()` | ~1157 | + +**Nonce:** All handlers verify nonce `disable_comments_save_settings`. The nonce is created in `admin_enqueue_scripts()` (~line 799) and exposed to JS as `disableCommentsObj._nonce`. + +**POST data parsing:** `get_form_array_escaped()` (~line 1202) reads `$_POST['data']` as a URL-encoded string, parses with `wp_parse_args()`, and sanitizes all values with `map_deep(..., 'sanitize_text_field')`. + +**Network admin flag:** `$formArray['is_network_admin']` comes from POST data and controls network-wide operations — always verify server-side capability before acting on it. + +--- + +## Development + +```bash +npm install # Install JS build deps +npm run build # Compile JS/CSS via Grunt + Babel +npm run release # Build + generate .pot + package release +``` + +```bash +composer install # Install PHP dev deps (Brain/Monkey for tests) +./vendor/bin/phpunit # Run tests +``` + +**Linting:** `phpcs.ruleset.xml` is configured for WordPress Coding Standards. + +--- + +## Architecture Notes + +- **Singleton pattern:** Always access via `Disable_Comments::get_instance()`. +- **CLI support:** `includes/cli.php` calls the same handler methods with `$_args` to bypass nonce (expected for WP-CLI context; nonce bypass is gated on `$this->is_CLI`). +- **Multisite vs single-site:** Plugin behaviour branches heavily on `$this->networkactive` (set in constructor) and `$this->sitewide_settings`. +- **Database queries:** Use `$wpdb->prepare()` throughout `delete_comments()`. Safe against SQL injection. +- **Input sanitization:** `get_form_array_escaped()` uses `wp_parse_args()` + `map_deep(sanitize_text_field)`. From c6714a392952a44b96aaea576c0fbf6c0c3566d7 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:30:02 +0600 Subject: [PATCH 03/18] Security: remove HTTP_REFERER trust from is_network_admin() #4 Replace Referer-based AJAX context check with capability-based check. Sub-site admins could forge the Referer header to gain network-admin privileges in AJAX handlers. Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/disable-comments.php b/disable-comments.php index 4d83185..6f10473 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -116,11 +116,7 @@ function __construct() { } public function is_network_admin() { - $sanitized_referer = isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : ''; - if (is_network_admin() || !empty($sanitized_referer) && defined('DOING_AJAX') && DOING_AJAX && is_multisite() && preg_match('#^' . network_admin_url() . '#i', $sanitized_referer)) { - return true; - } - return false; + return is_network_admin(); } /** * Enable CLI From e3ed4b9daa25d4fed7a5ea24eb28b961e95aeec8 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:30:14 +0600 Subject: [PATCH 04/18] Security: add capability check to disable_comments_settings() #1 Sub-site admins could POST is_network_admin=1 to apply network-wide config changes without Super Admin privileges. Add current_user_can() check after nonce verification. Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/disable-comments.php b/disable-comments.php index 6f10473..a3db3e3 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1214,6 +1214,11 @@ public function disable_comments_settings($_args = array()) { $nonce = (isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''); if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { + $required_cap = $this->is_network_admin() ? 'manage_network_plugins' : 'manage_options'; + if (!$this->is_CLI && !current_user_can($required_cap)) { + wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); + } + $formArray = $this->get_form_array_escaped($_args); $old_options = $this->options; From 8921f5d814c85a6a42452916e0fdccf337789fc0 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:30:25 +0600 Subject: [PATCH 05/18] Security: add capability and per-blog auth checks to delete_comments_settings() #2 Sub-site admins could supply arbitrary blog_ids to permanently delete comments from any site in the network. Add current_user_can() check and per-blog membership validation before switch_to_blog(). Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/disable-comments.php b/disable-comments.php index a3db3e3..538200a 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1330,6 +1330,13 @@ public function delete_comments_settings($_args = array()) { if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { $formArray = $this->get_form_array_escaped($_args); + if (!$this->is_CLI) { + $required_cap = $this->is_network_admin() ? 'manage_network_plugins' : 'manage_options'; + if (!current_user_can($required_cap)) { + wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); + } + } + if (!empty($formArray['is_network_admin']) && function_exists('get_sites') && class_exists('WP_Site_Query')) { $sites = get_sites([ 'number' => 0, @@ -1338,6 +1345,9 @@ public function delete_comments_settings($_args = array()) { foreach ($sites as $blog_id) { // $formArray['disabled_sites'] ids don't include "site_" prefix. if (!empty($formArray['disabled_sites']) && !empty($formArray['disabled_sites']["site_$blog_id"])) { + if (!is_super_admin() && !is_user_member_of_blog(get_current_user_id(), $blog_id)) { + continue; // Skip sites the user doesn't belong to + } switch_to_blog($blog_id); $log = $this->delete_comments($_args); restore_current_blog(); From 854eb86b29917aff0bbb03fdc35865dbc156d916 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:30:59 +0600 Subject: [PATCH 06/18] Security: escape role names to prevent DOM XSS in role exclusion UI #3 Malicious role names containing HTML could execute in the admin session when selected for exclusion. Fix PHP source (esc_html on translate_user_role) and JS sink (encode labels before .html()). Co-Authored-By: Claude Sonnet 4.6 --- assets/js/disable-comments-settings-scripts.js | 10 ++++++++-- disable-comments.php | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/js/disable-comments-settings-scripts.js b/assets/js/disable-comments-settings-scripts.js index 733d071..059ce99 100644 --- a/assets/js/disable-comments-settings-scripts.js +++ b/assets/js/disable-comments-settings-scripts.js @@ -432,7 +432,10 @@ jQuery(document).ready(function ($) { }).map(function(val, index){ return val.id; }); - var text = "" + _selectedOptions.join(", ") + ""; + var escapedOptions = _selectedOptions.map(function(label) { + return $('').text(label).html(); + }); + var text = "" + escapedOptions.join(", ") + ""; excludedRoles.html(sprintf(__("Comments are visible to %s and Logged out users.", "disable-comments"), text)); includedRoles.text(__("No comments will be visible to other roles.", "disable-comments")); } @@ -441,7 +444,10 @@ jQuery(document).ready(function ($) { var selectedOptionsLabels = selectedOptions.map(function(val, index){ return val.text; }); - var text = "" + selectedOptionsLabels.join(", ") + ""; + var escapedLabels = selectedOptionsLabels.map(function(label) { + return $('').text(label).html(); + }); + var text = "" + escapedLabels.join(", ") + ""; excludedRoles.html(sprintf(__("Comments are visible to %s.", "disable-comments"), text)); includedRoles.text(__("Other roles and logged out users won't see any comments.", "disable-comments")); } diff --git a/disable-comments.php b/disable-comments.php index 538200a..dfd6c6c 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1115,7 +1115,7 @@ public function get_roles($selected) { foreach ($editable_roles as $role => $details) { $roles[] = [ "id" => esc_attr($role), - "text" => translate_user_role($details['name']), + "text" => esc_html(translate_user_role($details['name'])), "selected" => in_array($role, (array) $selected), ]; } From 6cd801a10d2bc7cf6a262546459cd46850cdf599 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:30:38 +0600 Subject: [PATCH 07/18] Security: add capability check to get_sub_sites() to prevent subsite enumeration #5 Any authenticated user could enumerate all subsite names, IDs, and comment-disable status across the network via the AJAX endpoint. Restrict to users with manage_network_plugins or manage_options. Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/disable-comments.php b/disable-comments.php index dfd6c6c..1d07488 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1158,6 +1158,10 @@ public function get_sub_sites() { wp_send_json(['data' => [], 'totalNumber' => 0]); } + if (!current_user_can('manage_network_plugins') && !current_user_can('manage_options')) { + wp_send_json(['data' => [], 'totalNumber' => 0]); + } + $_sub_sites = []; $type = isset($_GET['type']) ? sanitize_text_field(wp_unslash($_GET['type'])) : 'disabled'; $search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : ''; From 60bd5f3ebb0dbc4e6c2e6b17173de7ba1be1093e Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 12:41:10 +0600 Subject: [PATCH 08/18] Exclude .claude/ worktrees from git tracking and distribution zips Co-Authored-By: Claude Sonnet 4.6 --- .distignore | 1 + .gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.distignore b/.distignore index 68cca0f..af19539 100644 --- a/.distignore +++ b/.distignore @@ -39,4 +39,5 @@ assets/img/card *.map assets/scss .ai/ +.claude/ CLAUDE.md diff --git a/.gitignore b/.gitignore index 82c1dfc..9466465 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules/ vendor/ .vscode/ .idea +.claude/worktrees/ package-lock.json pnpm-lock.yaml .ai/security/ From c9b578ac5db715d0ad5f5a84c8725de81fce752e Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 13:04:43 +0600 Subject: [PATCH 09/18] Security: derive network context from POST data, not is_network_admin() Fix #4 regression: WordPress is_network_admin() always returns false during AJAX, so the capability checks in disable_comments_settings() and delete_comments_settings() always fell back to manage_options, allowing site admins to still trigger network-wide operations. Also restores network-wide avatar management which was silently broken by the same regression (line 1269 branch never reached). Fix: read formArray before the capability check, derive context from the trusted-and-sanitized is_network_admin POST flag, and gate the avatar network loop on both the flag and manage_network_plugins. Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/disable-comments.php b/disable-comments.php index 1d07488..98f99f8 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1218,13 +1218,13 @@ public function disable_comments_settings($_args = array()) { $nonce = (isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''); if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { - $required_cap = $this->is_network_admin() ? 'manage_network_plugins' : 'manage_options'; + $formArray = $this->get_form_array_escaped($_args); + $is_network_action = !empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1'; + $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!$this->is_CLI && !current_user_can($required_cap)) { wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); } - $formArray = $this->get_form_array_escaped($_args); - $old_options = $this->options; $this->options = []; if ($this->is_CLI) { @@ -1266,7 +1266,7 @@ public function disable_comments_settings($_args = array()) { } if (isset($formArray['disable_avatar'])) { - if ($this->is_network_admin()) { + if ($is_network_action && current_user_can('manage_network_plugins')) { if ($formArray['disable_avatar'] == '0' || $formArray['disable_avatar'] == '1') { $sites = get_sites([ 'number' => 0, @@ -1335,7 +1335,8 @@ public function delete_comments_settings($_args = array()) { $formArray = $this->get_form_array_escaped($_args); if (!$this->is_CLI) { - $required_cap = $this->is_network_admin() ? 'manage_network_plugins' : 'manage_options'; + $is_network_action = !empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1'; + $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!current_user_can($required_cap)) { wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); } From 9e9fe6d12a77e16fa455126c1fe814473fe1dca5 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 14:11:15 +0600 Subject: [PATCH 10/18] Exclude pnpm-lock.yaml from distribution Co-Authored-By: Claude Sonnet 4.6 --- .distignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.distignore b/.distignore index af19539..62f6234 100644 --- a/.distignore +++ b/.distignore @@ -19,6 +19,7 @@ composer.lock Gruntfile.js package.json package-lock.json +pnpm-lock.yaml prepros.config phpunit.xml phpunit.xml.dist From 9fa11c98390d3ff7123549a806a409c97c1d39f1 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 14:54:43 +0600 Subject: [PATCH 11/18] Fix esc_html__ stripping HTML tags in admin notice Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disable-comments.php b/disable-comments.php index b57aaa7..8a2aa46 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -825,7 +825,7 @@ public function discussion_notice() { } // translators: %s: disabled post types. - echo '

' . sprintf(esc_html__('Note: The Disable Comments plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(esc_html__(', ', 'disable-comments'), $names_escaped)) . '

'; + echo '

' . sprintf(__('Note: The Disable Comments plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(__(', ', 'disable-comments'), $names_escaped)) . '

'; } } From b99618277bd29c01a02a16ecc55507c940816083 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 15:42:28 +0600 Subject: [PATCH 12/18] Security: gate update_site_option on network action capability check Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disable-comments.php b/disable-comments.php index 8a2aa46..5c22b6d 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1261,7 +1261,7 @@ public function disable_comments_settings($_args = array()) { $this->options['extra_post_types'] = array_diff($extra_post_types, array_keys($post_types)); // Make sure we don't double up builtins. } - if (isset($formArray['sitewide_settings'])) { + if ($is_network_action && isset($formArray['sitewide_settings'])) { update_site_option('disable_comments_sitewide_settings', $formArray['sitewide_settings']); } From 6e5a77644f590cbdd263ffdb9bfc967f5f5e420f Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 16:49:22 +0600 Subject: [PATCH 13/18] Fix is_network_admin() AJAX context and unify network action detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - is_network_admin() now detects network context in AJAX via ?is_network_admin=1 URL param, but only when current user has manage_network_plugins — eliminates spoofing concern and fixes constructor loading wrong options during AJAX - JS passes is_network_admin=1 in URL for all three AJAX calls when on network admin - Both handlers now derive $is_network_action from $this->is_network_admin() for AJAX (single authoritative source) and fall back to $formArray for CLI - disable_comments_settings() explicitly loads get_site_option() for $old_options on network actions, preventing disabled_sites corruption via AJAX - get_sub_sites() now requires manage_network_plugins on multisite (vs OR logic that allowed any site admin to enumerate all subsites) - Removes redundant current_user_can() re-check on avatar loop Co-Authored-By: Claude Sonnet 4.6 --- .../js/disable-comments-settings-scripts.js | 7 ++-- disable-comments.php | 34 +++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/assets/js/disable-comments-settings-scripts.js b/assets/js/disable-comments-settings-scripts.js index 059ce99..77e5fb2 100644 --- a/assets/js/disable-comments-settings-scripts.js +++ b/assets/js/disable-comments-settings-scripts.js @@ -6,6 +6,7 @@ jQuery(document).ready(function ($) { var saveBtn = jQuery("#disableCommentSaveSettings button.button.button__success"); var deleteBtn = jQuery("#deleteCommentSettings button.button.button__delete"); var savedData; + var networkAjaxUrl = ajaxurl + (disableCommentsObj.is_network_admin === '1' ? '?is_network_admin=1' : ''); if(jQuery('.sites_list_wrapper').length){ var addSite = function($sites_list, site, type){ @@ -49,7 +50,7 @@ jQuery(document).ready(function ($) { var $pageSizeWrapper = $sites_list_wrapper.find('.page__size__wrapper'); var isPageLoaded = {}; var args = { - dataSource : ajaxurl, + dataSource : networkAjaxUrl, locator : 'data', pageSize : $pageSize.val() || 50, showPageNumbers : false, @@ -296,7 +297,7 @@ jQuery(document).ready(function ($) { }; jQuery.ajax({ - url: ajaxurl, + url: networkAjaxUrl, type: "post", data: data, beforeSend: function () { @@ -360,7 +361,7 @@ jQuery(document).ready(function ($) { deleteBtn.html( '' + __("Deleting Comments..", "disable-comments") + '' ); - jQuery.post(ajaxurl, data, function (response) { + jQuery.post(networkAjaxUrl, data, function (response) { deleteBtn.html(__("Delete Comments", "disable-comments")); if (response.success) { Swal.fire({ diff --git a/disable-comments.php b/disable-comments.php index 5c22b6d..c1fa190 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -116,7 +116,15 @@ function __construct() { } public function is_network_admin() { - return is_network_admin(); + if (is_network_admin()) { + return true; + } + if (defined('DOING_AJAX') && DOING_AJAX) { + return !empty($_GET['is_network_admin']) + && $_GET['is_network_admin'] === '1' + && current_user_can('manage_network_plugins'); + } + return false; } /** * Enable CLI @@ -792,7 +800,8 @@ public function settings_page_assets($hook_suffix) { 'save_action' => 'disable_comments_save_settings', 'delete_action' => 'disable_comments_delete_comments', 'settings_URI' => $this->settings_page_url(), - '_nonce' => wp_create_nonce('disable_comments_save_settings') + '_nonce' => wp_create_nonce('disable_comments_save_settings'), + 'is_network_admin' => is_network_admin() ? '1' : '0' ) ); wp_set_script_translations('disable-comments-scripts', 'disable-comments'); @@ -1158,8 +1167,9 @@ public function get_sub_sites() { wp_send_json(['data' => [], 'totalNumber' => 0]); } - if (!current_user_can('manage_network_plugins') && !current_user_can('manage_options')) { - wp_send_json(['data' => [], 'totalNumber' => 0]); + $required_cap = is_multisite() ? 'manage_network_plugins' : 'manage_options'; + if (!current_user_can($required_cap)) { + wp_send_json_error(['message' => __('Sorry, you are not allowed to access this resource.', 'disable-comments')], 403); } $_sub_sites = []; @@ -1219,19 +1229,21 @@ public function disable_comments_settings($_args = array()) { if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { $formArray = $this->get_form_array_escaped($_args); - $is_network_action = !empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1'; + $is_network_action = $this->is_CLI + ? (!empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1') + : $this->is_network_admin(); $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!$this->is_CLI && !current_user_can($required_cap)) { wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); } - $old_options = $this->options; + $old_options = $this->is_CLI ? $this->options : ($is_network_action ? get_site_option('disable_comments_options', []) : $this->options); $this->options = []; if ($this->is_CLI) { $this->options = $old_options; } - $this->options['is_network_admin'] = isset($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1' ? true : false; + $this->options['is_network_admin'] = $is_network_action; if (!empty($this->options['is_network_admin']) && function_exists('get_sites') && empty($formArray['sitewide_settings'])) { $formArray['disabled_sites'] = isset($formArray['disabled_sites']) ? $formArray['disabled_sites'] : []; @@ -1266,7 +1278,7 @@ public function disable_comments_settings($_args = array()) { } if (isset($formArray['disable_avatar'])) { - if ($is_network_action && current_user_can('manage_network_plugins')) { + if ($is_network_action) { if ($formArray['disable_avatar'] == '0' || $formArray['disable_avatar'] == '1') { $sites = get_sites([ 'number' => 0, @@ -1333,16 +1345,18 @@ public function delete_comments_settings($_args = array()) { if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { $formArray = $this->get_form_array_escaped($_args); + $is_network_action = $this->is_CLI + ? (!empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1') + : $this->is_network_admin(); if (!$this->is_CLI) { - $is_network_action = !empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1'; $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!current_user_can($required_cap)) { wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); } } - if (!empty($formArray['is_network_admin']) && function_exists('get_sites') && class_exists('WP_Site_Query')) { + if ($is_network_action && function_exists('get_sites') && class_exists('WP_Site_Query')) { $sites = get_sites([ 'number' => 0, 'fields' => 'ids', From 78478d04d7c06f2257112c72f2c1173f87e41d8b Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 17:23:07 +0600 Subject: [PATCH 14/18] Address Copilot review suggestions from PR #160 - Add HTTP 403 status to wp_send_json_error() permission failures in both AJAX handlers for consistency with get_sub_sites() - Sanitize $_REQUEST['is_network_admin'] with wp_unslash/sanitize_text_field per WordPress coding standards - Fix networkAjaxUrl to use ?/& correctly if ajaxurl already has query params - Use esc_html__() for plain-text Site Health value (no markup needed) - Wrap discussion_notice() output in wp_kses_post() to guard against translator-injected markup Co-Authored-By: Claude Sonnet 4.6 --- assets/js/disable-comments-settings-scripts.js | 4 +++- disable-comments.php | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/assets/js/disable-comments-settings-scripts.js b/assets/js/disable-comments-settings-scripts.js index 77e5fb2..1a1d720 100644 --- a/assets/js/disable-comments-settings-scripts.js +++ b/assets/js/disable-comments-settings-scripts.js @@ -6,7 +6,9 @@ jQuery(document).ready(function ($) { var saveBtn = jQuery("#disableCommentSaveSettings button.button.button__success"); var deleteBtn = jQuery("#deleteCommentSettings button.button.button__delete"); var savedData; - var networkAjaxUrl = ajaxurl + (disableCommentsObj.is_network_admin === '1' ? '?is_network_admin=1' : ''); + var networkAjaxUrl = disableCommentsObj.is_network_admin === '1' + ? ajaxurl + (ajaxurl.indexOf('?') === -1 ? '?' : '&') + 'is_network_admin=1' + : ajaxurl; if(jQuery('.sites_list_wrapper').length){ var addSite = function($sites_list, site, type){ diff --git a/disable-comments.php b/disable-comments.php index c1fa190..926acb7 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -120,9 +120,8 @@ public function is_network_admin() { return true; } if (defined('DOING_AJAX') && DOING_AJAX) { - return !empty($_GET['is_network_admin']) - && $_GET['is_network_admin'] === '1' - && current_user_can('manage_network_plugins'); + $is_network_admin_param = isset($_REQUEST['is_network_admin']) ? sanitize_text_field(wp_unslash($_REQUEST['is_network_admin'])) : ''; + return $is_network_admin_param === '1' && current_user_can('manage_network_plugins'); } return false; } @@ -834,7 +833,7 @@ public function discussion_notice() { } // translators: %s: disabled post types. - echo '

' . sprintf(__('Note: The Disable Comments plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(__(', ', 'disable-comments'), $names_escaped)) . '

'; + echo '

' . wp_kses_post(sprintf(__('Note: The Disable Comments plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(__(', ', 'disable-comments'), $names_escaped))) . '

'; } } @@ -1234,7 +1233,7 @@ public function disable_comments_settings($_args = array()) { : $this->is_network_admin(); $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!$this->is_CLI && !current_user_can($required_cap)) { - wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); + wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); } $old_options = $this->is_CLI ? $this->options : ($is_network_action ? get_site_option('disable_comments_options', []) : $this->options); @@ -1352,7 +1351,7 @@ public function delete_comments_settings($_args = array()) { if (!$this->is_CLI) { $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!current_user_can($required_cap)) { - wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); + wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); } } @@ -1880,7 +1879,7 @@ public function add_site_health_info($debug_info) { ), 'disabled_post_type_count' => array( 'label' => __('Disabled Post Types Count', 'disable-comments'), - 'value' => sprintf(__('%1$d of %2$d', 'disable-comments'), count($data['disabled_post_types']), $data['total_post_types']), + 'value' => sprintf(esc_html__('%1$d of %2$d', 'disable-comments'), count($data['disabled_post_types']), $data['total_post_types']), ), 'disabled_post_types' => array( 'label' => __('Disabled Post Types', 'disable-comments'), From 2c86f7f0f85bb7b1f32e8ee35323b7fa900aa1a5 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 17:43:38 +0600 Subject: [PATCH 15/18] Fix UI error handling and double-encoding in role names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_sub_sites() nonce failure now returns proper 403 error instead of HTTP 200 with empty success-shaped data - Remove esc_html() on role names before JSON encoding — caused double- encoding since JS already escapes via $('').text() - Save button: add else branch to reset button and show error when response.success is false with HTTP 200 - Delete button: add .fail() handler so button resets on HTTP error (403, network failure) instead of staying stuck in Deleting state - Swal delete messages: use text: instead of html: to prevent XSS Co-Authored-By: Claude Sonnet 4.6 --- assets/js/disable-comments-settings-scripts.js | 18 ++++++++++++++++-- disable-comments.php | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/assets/js/disable-comments-settings-scripts.js b/assets/js/disable-comments-settings-scripts.js index 1a1d720..262f75f 100644 --- a/assets/js/disable-comments-settings-scripts.js +++ b/assets/js/disable-comments-settings-scripts.js @@ -319,6 +319,13 @@ jQuery(document).ready(function ($) { }); saveBtn.removeClass('form-dirty').prop('disabled', true); savedData = $form.serialize(); + } else { + saveBtn.html(__("Save Settings", "disable-comments")); + Swal.fire({ + icon: "error", + title: __("Oops...", "disable-comments"), + text: response.data && response.data.message ? response.data.message : __("Something went wrong!", "disable-comments"), + }); } }, error: function () { @@ -369,7 +376,7 @@ jQuery(document).ready(function ($) { Swal.fire({ icon: "success", title: __("Deleted", "disable-comments"), - html: response.data.message, + text: response.data.message, timer: 3000, showConfirmButton: false, }); @@ -377,10 +384,17 @@ jQuery(document).ready(function ($) { Swal.fire({ icon: "error", title: __("Oops...", "disable-comments"), - html: response.data.message, + text: response.data && response.data.message ? response.data.message : __("Something went wrong!", "disable-comments"), showConfirmButton: true, }); } + }).fail(function () { + deleteBtn.html(__("Delete Comments", "disable-comments")); + Swal.fire({ + icon: "error", + title: __("Oops...", "disable-comments"), + text: __("Something went wrong!", "disable-comments"), + }); }); } }); diff --git a/disable-comments.php b/disable-comments.php index 926acb7..1c0f077 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1123,7 +1123,7 @@ public function get_roles($selected) { foreach ($editable_roles as $role => $details) { $roles[] = [ "id" => esc_attr($role), - "text" => esc_html(translate_user_role($details['name'])), + "text" => translate_user_role($details['name']), "selected" => in_array($role, (array) $selected), ]; } @@ -1163,7 +1163,7 @@ public function settings_page() { public function get_sub_sites() { $nonce = (isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : ''); if (!wp_verify_nonce($nonce, 'disable_comments_save_settings')) { - wp_send_json(['data' => [], 'totalNumber' => 0]); + wp_send_json_error(['message' => __('Invalid request. Please refresh the page and try again.', 'disable-comments')], 403); } $required_cap = is_multisite() ? 'manage_network_plugins' : 'manage_options'; From 5e6657a5893b6fb60c31c17ac86cceae07354876 Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Sun, 29 Mar 2026 17:54:54 +0600 Subject: [PATCH 16/18] Handle new server error responses in JS - get_sub_sites: add formatAjaxError to pagination args so nonce/cap failures show a Swal error instead of silently rendering empty list - Save error handler: extract server message from jqXHR.responseJSON, fix hardcoded button label to use __(), fix old Swal type: -> icon: - Delete fail handler: extract server message from jqXHR.responseJSON Co-Authored-By: Claude Sonnet 4.6 --- .../js/disable-comments-settings-scripts.js | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/assets/js/disable-comments-settings-scripts.js b/assets/js/disable-comments-settings-scripts.js index 262f75f..fa55a30 100644 --- a/assets/js/disable-comments-settings-scripts.js +++ b/assets/js/disable-comments-settings-scripts.js @@ -77,6 +77,12 @@ jQuery(document).ready(function ($) { }, }; }, + formatAjaxError: function(jqXHR) { + var msg = jqXHR.responseJSON && jqXHR.responseJSON.data && jqXHR.responseJSON.data.message + ? jqXHR.responseJSON.data.message + : __('Something went wrong!', 'disable-comments'); + Swal.fire({ icon: 'error', title: __('Oops...', 'disable-comments'), text: msg }); + }, callback : function(data, pagination) { var pageNumber = pagination.pageNumber; addSites($sites_list, data, type); @@ -328,12 +334,15 @@ jQuery(document).ready(function ($) { }); } }, - error: function () { - saveBtn.html("Save Settings"); + error: function (jqXHR) { + saveBtn.html(__("Save Settings", "disable-comments")); + var msg = jqXHR.responseJSON && jqXHR.responseJSON.data && jqXHR.responseJSON.data.message + ? jqXHR.responseJSON.data.message + : __("Something went wrong!", "disable-comments"); Swal.fire({ - type: "error", + icon: "error", title: __("Oops...", "disable-comments"), - text: __("Something went wrong!", "disable-comments"), + text: msg, }); }, }); @@ -388,12 +397,15 @@ jQuery(document).ready(function ($) { showConfirmButton: true, }); } - }).fail(function () { + }).fail(function (jqXHR) { deleteBtn.html(__("Delete Comments", "disable-comments")); + var msg = jqXHR.responseJSON && jqXHR.responseJSON.data && jqXHR.responseJSON.data.message + ? jqXHR.responseJSON.data.message + : __("Something went wrong!", "disable-comments"); Swal.fire({ icon: "error", title: __("Oops...", "disable-comments"), - text: __("Something went wrong!", "disable-comments"), + text: msg, }); }); } From e41335c0994e93ef79c2094199cda4c0c99a4e4a Mon Sep 17 00:00:00 2001 From: "Md. Alimuzzaman Alim" Date: Mon, 30 Mar 2026 11:02:59 +0600 Subject: [PATCH 17/18] Fix nonce failure silently returning success in delete_comments_settings() Previously, a failed nonce check would fall through to wp_send_json_success(), reporting a successful deletion with a "." message. Nonce is now verified as an early-exit guard that sends a 403 error and dies immediately. Also added wp_die() after the capability check error response for consistency. Co-Authored-By: Claude Sonnet 4.6 --- disable-comments.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/disable-comments.php b/disable-comments.php index 1c0f077..1a38496 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1342,7 +1342,14 @@ public function delete_comments_settings($_args = array()) { $log = ''; $nonce = (isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''); - if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { + if (!$this->is_CLI) { + if (!wp_verify_nonce($nonce, 'disable_comments_save_settings')) { + wp_send_json_error(['message' => __('Nonce verification failed.', 'disable-comments')], 403); + wp_die(); + } + } + + if (($this->is_CLI && !empty($_args)) || !$this->is_CLI) { $formArray = $this->get_form_array_escaped($_args); $is_network_action = $this->is_CLI ? (!empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1') @@ -1352,6 +1359,7 @@ public function delete_comments_settings($_args = array()) { $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; if (!current_user_can($required_cap)) { wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); + wp_die(); } } From 8693705025055bd76005c09496b87a1db730b2c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 05:18:21 +0000 Subject: [PATCH 18/18] Fix nonce failure silently returning success in disable_comments_settings() Agent-Logs-Url: https://github.com/WPDevelopers/disable-comments/sessions/8293ceb0-586f-466d-a783-948b52cc6525 Co-authored-by: alimuzzaman <6744156+alimuzzaman@users.noreply.github.com> --- disable-comments.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/disable-comments.php b/disable-comments.php index 1a38496..759eb7d 100644 --- a/disable-comments.php +++ b/disable-comments.php @@ -1225,7 +1225,14 @@ public function get_form_array_escaped($_args = array()) { public function disable_comments_settings($_args = array()) { $nonce = (isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''); - if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { + + if (!$this->is_CLI) { + if (!wp_verify_nonce($nonce, 'disable_comments_save_settings')) { + wp_send_json_error(['message' => __('Nonce verification failed.', 'disable-comments')], 403); + } + } + + if (($this->is_CLI && !empty($_args)) || !$this->is_CLI) { $formArray = $this->get_form_array_escaped($_args); $is_network_action = $this->is_CLI