Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
95e00ff
Improve I18N Issue based on 2.5.3
alexclassroom Sep 15, 2025
a60017c
Merge branch 'WPDevelopers:master' into master
alexclassroom Nov 5, 2025
e6fd88a
Merge branch 'WPDevelopers:master' into master
alexclassroom Dec 3, 2025
24752f0
Merge branch 'WPDevelopers:master' into master
alexclassroom Jan 26, 2026
acbc92f
Add AI agent context, CLAUDE.md, and dev tooling exclusions
alimuzzaman Mar 29, 2026
c6714a3
Security: remove HTTP_REFERER trust from is_network_admin() #4
alimuzzaman Mar 29, 2026
e3ed4b9
Security: add capability check to disable_comments_settings() #1
alimuzzaman Mar 29, 2026
8921f5d
Security: add capability and per-blog auth checks to delete_comments_…
alimuzzaman Mar 29, 2026
854eb86
Security: escape role names to prevent DOM XSS in role exclusion UI #3
alimuzzaman Mar 29, 2026
6cd801a
Security: add capability check to get_sub_sites() to prevent subsite …
alimuzzaman Mar 29, 2026
60bd5f3
Exclude .claude/ worktrees from git tracking and distribution zips
alimuzzaman Mar 29, 2026
c9b578a
Security: derive network context from POST data, not is_network_admin()
alimuzzaman Mar 29, 2026
9e9fe6d
Exclude pnpm-lock.yaml from distribution
alimuzzaman Mar 29, 2026
6f54eea
Merge pull request #156 from alexclassroom/master
alimuzzaman Mar 29, 2026
9fa11c9
Fix esc_html__ stripping HTML tags in admin notice
alimuzzaman Mar 29, 2026
b996182
Security: gate update_site_option on network action capability check
alimuzzaman Mar 29, 2026
6e5a776
Fix is_network_admin() AJAX context and unify network action detection
alimuzzaman Mar 29, 2026
78478d0
Address Copilot review suggestions from PR #160
alimuzzaman Mar 29, 2026
2c86f7f
Fix UI error handling and double-encoding in role names
alimuzzaman Mar 29, 2026
5e6657a
Handle new server error responses in JS
alimuzzaman Mar 29, 2026
e41335c
Fix nonce failure silently returning success in delete_comments_setti…
alimuzzaman Mar 30, 2026
8693705
Fix nonce failure silently returning success in disable_comments_sett…
Copilot Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .ai/README.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions .distignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ composer.lock
Gruntfile.js
package.json
package-lock.json
pnpm-lock.yaml
prepros.config
phpunit.xml
phpunit.xml.dist
Expand All @@ -38,3 +39,6 @@ src
assets/img/card
*.map
assets/scss
.ai/
.claude/
CLAUDE.md
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ node_modules/
vendor/
.vscode/
.idea
.claude/worktrees/
package-lock.json
pnpm-lock.yaml
.ai/security/
78 changes: 78 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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:** <https://wordpress.org/plugins/disable-comments/>
- **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)`.
57 changes: 46 additions & 11 deletions assets/js/disable-comments-settings-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +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 = 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){
Expand Down Expand Up @@ -49,7 +52,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,
Expand All @@ -74,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);
Expand Down Expand Up @@ -296,7 +305,7 @@ jQuery(document).ready(function ($) {
};

jQuery.ajax({
url: ajaxurl,
url: networkAjaxUrl,
type: "post",
data: data,
beforeSend: function () {
Expand All @@ -316,14 +325,24 @@ 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 () {
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,
});
},
});
Expand Down Expand Up @@ -360,24 +379,34 @@ jQuery(document).ready(function ($) {
deleteBtn.html(
'<svg id="eael-spinner" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 48 48"><circle cx="24" cy="4" r="4" fill="#fff"/><circle cx="12.19" cy="7.86" r="3.7" fill="#fffbf2"/><circle cx="5.02" cy="17.68" r="3.4" fill="#fef7e4"/><circle cx="5.02" cy="30.32" r="3.1" fill="#fef3d7"/><circle cx="12.19" cy="40.14" r="2.8" fill="#feefc9"/><circle cx="24" cy="44" r="2.5" fill="#feebbc"/><circle cx="35.81" cy="40.14" r="2.2" fill="#fde7af"/><circle cx="42.98" cy="30.32" r="1.9" fill="#fde3a1"/><circle cx="42.98" cy="17.68" r="1.6" fill="#fddf94"/><circle cx="35.81" cy="7.86" r="1.3" fill="#fcdb86"/></svg><span>' + __("Deleting Comments..", "disable-comments") + '</span>'
);
jQuery.post(ajaxurl, data, function (response) {
jQuery.post(networkAjaxUrl, data, function (response) {
deleteBtn.html(__("Delete Comments", "disable-comments"));
if (response.success) {
Swal.fire({
icon: "success",
title: __("Deleted", "disable-comments"),
html: response.data.message,
text: response.data.message,
timer: 3000,
showConfirmButton: false,
});
} else {
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 (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: msg,
});
});
}
});
Expand Down Expand Up @@ -432,7 +461,10 @@ jQuery(document).ready(function ($) {
}).map(function(val, index){
return val.id;
});
var text = "<b>" + _selectedOptions.join("</b>, <b>") + "</b>";
var escapedOptions = _selectedOptions.map(function(label) {
return $('<span>').text(label).html();
});
var text = "<b>" + escapedOptions.join("</b>, <b>") + "</b>";
excludedRoles.html(sprintf(__("Comments are visible to %s and <b>Logged out users</b>.", "disable-comments"), text));
includedRoles.text(__("No comments will be visible to other roles.", "disable-comments"));
}
Expand All @@ -441,7 +473,10 @@ jQuery(document).ready(function ($) {
var selectedOptionsLabels = selectedOptions.map(function(val, index){
return val.text;
});
var text = "<b>" + selectedOptionsLabels.join("</b>, <b>") + "</b>";
var escapedLabels = selectedOptionsLabels.map(function(label) {
return $('<span>').text(label).html();
});
var text = "<b>" + escapedLabels.join("</b>, <b>") + "</b>";
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"));
}
Expand Down
Loading