From ef103b26ed0155040f7fb3ddb43c71d4d412ebd5 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 4 Dec 2025 15:51:04 -0600 Subject: [PATCH 1/6] Wrap blocked domains and keywords tables in collapsible details element --- includes/wp-admin/class-settings-fields.php | 72 ++++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/includes/wp-admin/class-settings-fields.php b/includes/wp-admin/class-settings-fields.php index 24cd4adb56..a249330753 100644 --- a/includes/wp-admin/class-settings-fields.php +++ b/includes/wp-admin/class-settings-fields.php @@ -429,23 +429,35 @@ public static function render_moderation_section_description() { */ public static function render_site_blocked_domains_field() { $blocked_domains = Moderation::get_site_blocks()['domains']; + $count = \count( $blocked_domains ); ?>

- - - - - - - - +
+ + + + + + + + + + + +
@@ -463,23 +475,35 @@ public static function render_site_blocked_domains_field() { */ public static function render_site_blocked_keywords_field() { $blocked_keywords = Moderation::get_site_blocks()['keywords']; + $count = \count( $blocked_keywords ); ?>

- - - - - - - - +
+ + + + + + + + + + + +
From 5689b6f13b90b9f22e1640c637051844bb9583d1 Mon Sep 17 00:00:00 2001 From: Automattic Bot Date: Thu, 4 Dec 2025 22:51:59 +0100 Subject: [PATCH 2/6] Add changelog --- .github/changelog/2591-from-description | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/changelog/2591-from-description diff --git a/.github/changelog/2591-from-description b/.github/changelog/2591-from-description new file mode 100644 index 0000000000..92e4481c15 --- /dev/null +++ b/.github/changelog/2591-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Wrap blocked domains and keywords tables in collapsible details element. From 3d484775097d868f89fb9d887c08f1b8a5f1eb3d Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 4 Dec 2025 15:58:54 -0600 Subject: [PATCH 3/6] Fix escaping for sprintf output --- includes/wp-admin/class-settings-fields.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/includes/wp-admin/class-settings-fields.php b/includes/wp-admin/class-settings-fields.php index a249330753..cd554a81cf 100644 --- a/includes/wp-admin/class-settings-fields.php +++ b/includes/wp-admin/class-settings-fields.php @@ -438,10 +438,12 @@ public static function render_site_blocked_domains_field() {
@@ -484,10 +486,12 @@ public static function render_site_blocked_keywords_field() {
From 6205b3c3165cb0cf105b88aecd2af34344617ade Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Fri, 5 Dec 2025 08:33:36 -0600 Subject: [PATCH 4/6] Move inline CSS to stylesheet for site block details Move the inline styles for the collapsible details/summary elements to activitypub-admin.css instead of using inline styles in PHP. --- assets/css/activitypub-admin.css | 19 +++++++++++++++++++ includes/wp-admin/class-settings-fields.php | 16 ++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index ae415dbc66..5b3254654c 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -104,6 +104,25 @@ summary { color: #2271b1; } +.activitypub-site-block-details { + margin: 10px 0; +} + +.activitypub-site-block-details summary { + padding: 8px 0; + color: inherit; + text-decoration: none; +} + +.activitypub-site-block-details table { + max-width: 500px; + margin-top: 10px; +} + +.activitypub-site-block-details td:last-child { + width: 80px; +} + .activitypub-settings-accordion { border: 1px solid #c3c4c7; } diff --git a/includes/wp-admin/class-settings-fields.php b/includes/wp-admin/class-settings-fields.php index cd554a81cf..937ed5e1d3 100644 --- a/includes/wp-admin/class-settings-fields.php +++ b/includes/wp-admin/class-settings-fields.php @@ -435,8 +435,8 @@ public static function render_site_blocked_domains_field() {
-
- +
+ - + - ' ); } else if ( context === 'site' ) { - // For site moderation, add to the appropriate table - var container = $( '#new_site_' + type ).closest( '.activitypub-site-block-list' ); - var table = container.find( '.activitypub-site-blocked-' + type ); + // For site moderation, add to the table inside the details element + var details = $( '.activitypub-site-block-details[data-type="' + type + '"]' ); + table = details.find( '.activitypub-site-blocked-' + type ); + if ( table.length === 0 ) { - table = $( '' ); - container.find( '.add-site-block-form' ).before( table ); + // Create table inside the details element (after summary) + table = $( '' ); + details.find( 'summary' ).after( table ); } - table.append( '' + value + '' ); + + table.find( 'tbody' ).append( '' + value + '' ); + + updateSiteBlockSummary( type ); + } + } + + /** + * Helper function to update the site block summary count + */ + function updateSiteBlockSummary( type ) { + var details = $( '.activitypub-site-block-details[data-type="' + type + '"]' ); + var table = details.find( '.activitypub-site-blocked-' + type ); + var count = table.find( 'tbody tr' ).length || table.find( 'tr' ).length; + var summary = details.find( 'summary' ); + + if ( count === 0 ) { + // Empty state + var emptyText = type === 'domain' + ? __( 'No blocked domains', 'activitypub' ) + : __( 'No blocked keywords', 'activitypub' ); + summary.text( emptyText ); + details.attr( 'open', '' ); + table.remove(); + } else { + // Has items - use _n for proper pluralization + var text = type === 'domain' + ? _n( '%s blocked domain', '%s blocked domains', count, 'activitypub' ) + : _n( '%s blocked keyword', '%s blocked keywords', count, 'activitypub' ); + summary.text( sprintf( text, count ) ); } } @@ -63,13 +158,11 @@ if ( button.length > 0 ) { // Remove the parent table row - var parent = button.closest( 'tr' ); - var container = parent.closest( 'table' ); - parent.remove(); + button.closest( 'tr' ).remove(); - // If the container is now empty, remove it - if ( container.find( 'tr' ).length === 0 ) { - container.remove(); + // Update the summary count for site blocks + if ( context === 'site' ) { + updateSiteBlockSummary( type ); } } } @@ -94,33 +187,7 @@ var input = $( '#new_user_' + type ); var value = input.val().trim(); - if ( ! value ) { - // Use wp.a11y.speak for better accessibility. - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( activitypubModerationL10n.enterValue, 'assertive' ); - } else { - alert( activitypubModerationL10n.enterValue ); - } - return; - } - - // Validate domain format if this is a domain block - if ( type === 'domain' && ! isValidDomain( value ) ) { - var message = activitypubModerationL10n.invalidDomain || 'Please enter a valid domain (e.g., example.com).'; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } - alert( message ); - return; - } - - // Check if the term is already blocked - if ( isTermAlreadyBlocked( type, value, 'user', userId ) ) { - var message = activitypubModerationL10n.alreadyBlocked || 'This term is already blocked.'; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } - alert( message ); + if ( ! validateBlockedTerm( type, value, 'user', userId ) ) { return; } @@ -136,12 +203,8 @@ input.val( '' ); addBlockedTermToUI( type, value, 'user', userId ); }).fail( function( response ) { - var message = response && response.message ? response.message : activitypubModerationL10n.addBlockFailed; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } else { - alert( message ); - } + var message = response && response.message ? response.message : __( 'Failed to add block.', 'activitypub' ); + showMessage( message ); }); } @@ -157,12 +220,8 @@ }).done( function() { removeBlockedTermFromUI( type, value, 'user' ); }).fail( function( response ) { - var message = response && response.message ? response.message : activitypubModerationL10n.removeBlockFailed; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } else { - alert( message ); - } + var message = response && response.message ? response.message : __( 'Failed to remove block.', 'activitypub' ); + showMessage( message ); }); } @@ -204,32 +263,7 @@ var input = $( '#new_site_' + type ); var value = input.val().trim(); - if ( ! value ) { - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( activitypubModerationL10n.enterValue, 'assertive' ); - } else { - alert( activitypubModerationL10n.enterValue ); - } - return; - } - - // Validate domain format if this is a domain block - if ( type === 'domain' && ! isValidDomain( value ) ) { - var message = activitypubModerationL10n.invalidDomain || 'Please enter a valid domain (e.g., example.com).'; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } - alert( message ); - return; - } - - // Check if the term is already blocked - if ( isTermAlreadyBlocked( type, value, 'site' ) ) { - var message = activitypubModerationL10n.alreadyBlocked || 'This term is already blocked.'; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } - alert( message ); + if ( ! validateBlockedTerm( type, value, 'site', null ) ) { return; } @@ -244,12 +278,8 @@ input.val( '' ); addBlockedTermToUI( type, value, 'site' ); }).fail( function( response ) { - var message = response && response.message ? response.message : activitypubModerationL10n.addBlockFailed; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } else { - alert( message ); - } + var message = response && response.message ? response.message : __( 'Failed to add block.', 'activitypub' ); + showMessage( message ); }); } @@ -264,12 +294,8 @@ }).done( function() { removeBlockedTermFromUI( type, value, 'site' ); }).fail( function( response ) { - var message = response && response.message ? response.message : activitypubModerationL10n.removeBlockFailed; - if ( wp.a11y && wp.a11y.speak ) { - wp.a11y.speak( message, 'assertive' ); - } else { - alert( message ); - } + var message = response && response.message ? response.message : __( 'Failed to remove block.', 'activitypub' ); + showMessage( message ); }); } @@ -302,4 +328,4 @@ // Initialize when document is ready. $( document ).ready( init ); -})( jQuery ); +})( jQuery, wp ); diff --git a/includes/wp-admin/class-admin.php b/includes/wp-admin/class-admin.php index e3b4ca4cfd..af99fc2032 100644 --- a/includes/wp-admin/class-admin.php +++ b/includes/wp-admin/class-admin.php @@ -342,22 +342,23 @@ public static function enqueue_moderation_scripts() { \wp_enqueue_script( 'activitypub-moderation-admin', ACTIVITYPUB_PLUGIN_URL . 'assets/js/activitypub-moderation-admin.js', - array( 'jquery', 'wp-util', 'wp-a11y' ), + array( 'jquery', 'wp-util', 'wp-a11y', 'wp-i18n' ), ACTIVITYPUB_PLUGIN_VERSION, true ); + \wp_set_script_translations( + 'activitypub-moderation-admin', + 'activitypub', + ACTIVITYPUB_PLUGIN_DIR . 'languages' + ); + // Localize script with translations and nonces. \wp_localize_script( 'activitypub-moderation-admin', 'activitypubModerationL10n', array( - 'enterValue' => \__( 'Please enter a value to block.', 'activitypub' ), - 'addBlockFailed' => \__( 'Failed to add block.', 'activitypub' ), - 'removeBlockFailed' => \__( 'Failed to remove block.', 'activitypub' ), - 'alreadyBlocked' => \__( 'This term is already blocked.', 'activitypub' ), - 'invalidDomain' => \__( 'Please enter a valid domain (e.g., example.com).', 'activitypub' ), - 'nonce' => \wp_create_nonce( 'activitypub_moderation_settings' ), + 'nonce' => \wp_create_nonce( 'activitypub_moderation_settings' ), ) ); } diff --git a/includes/wp-admin/class-settings-fields.php b/includes/wp-admin/class-settings-fields.php index 937ed5e1d3..d5065f77fa 100644 --- a/includes/wp-admin/class-settings-fields.php +++ b/includes/wp-admin/class-settings-fields.php @@ -434,21 +434,26 @@ public static function render_site_blocked_domains_field() {

- -
+
> - + 0 ) : ?> + + + + + - + + - + + +
-
@@ -482,21 +488,26 @@ public static function render_site_blocked_keywords_field() {

- -
+
> - + 0 ) : ?> + + + + + - + + - + + +
-
From c0d132f60617eb9d2d3d88b80dcca97f578c7f25 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Mon, 8 Dec 2025 10:02:31 -0600 Subject: [PATCH 6/6] Fix XSS vulnerability by using DOM-safe methods for user input Use jQuery's element construction with attributes object and .text() instead of string concatenation when inserting user-controlled values into the DOM. This prevents malicious input from being executed. --- assets/js/activitypub-moderation-admin.js | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/assets/js/activitypub-moderation-admin.js b/assets/js/activitypub-moderation-admin.js index 0a891cfbda..207d41c16a 100644 --- a/assets/js/activitypub-moderation-admin.js +++ b/assets/js/activitypub-moderation-admin.js @@ -89,6 +89,26 @@ return true; } + /** + * Create a table row for a blocked term. + * + * @param {string} type - The type of block (domain or keyword) + * @param {string} value - The blocked value + * @param {string} context - The context (user or site) + * @return {jQuery} The constructed table row + */ + function createBlockedTermRow( type, value, context ) { + var $button = $( '' ); + table.find( 'tbody' ).append( createBlockedTermRow( type, value, context ) ); } else if ( context === 'site' ) { // For site moderation, add to the table inside the details element var details = $( '.activitypub-site-block-details[data-type="' + type + '"]' ); @@ -116,7 +136,7 @@ details.find( 'summary' ).after( table ); } - table.find( 'tbody' ).append( '' + value + '' ); + table.find( 'tbody' ).append( createBlockedTermRow( type, value, context ) ); updateSiteBlockSummary( type ); }