From 2912ade0ada165471beff06a7c9c76d3eafc06cc Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 20 May 2026 13:23:41 -0400 Subject: [PATCH 1/8] Fix missing capability check in add_ignore AJAX handler Any logged-in user could call edac_insert_ignore_data to dismiss issues on posts they cannot edit. Add current_user_can('edit_post') check using the same postid lookup pattern as the REST /dismiss-issue endpoint. Co-Authored-By: Claude Sonnet 4.6 --- admin/class-ajax.php | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/admin/class-ajax.php b/admin/class-ajax.php index 545b3b75a..ca2e37b5b 100644 --- a/admin/class-ajax.php +++ b/admin/class-ajax.php @@ -819,17 +819,32 @@ public function add_ignore() { } global $wpdb; - $table_name = $wpdb->prefix . 'accessibility_checker'; - $raw_ids = isset( $_REQUEST['ids'] ) ? (array) wp_unslash( $_REQUEST['ids'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled below. - $ids = array_map( + $table_name = $wpdb->prefix . 'accessibility_checker'; + $raw_ids = isset( $_REQUEST['ids'] ) ? (array) wp_unslash( $_REQUEST['ids'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled below. + $ids = array_map( function ( $value ) { return (int) $value; }, $raw_ids ); // Sanitizing array elements to integers. - $action = isset( $_REQUEST['ignore_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_action'] ) ) : ''; - $type = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : ''; - $siteid = get_current_blog_id(); + $action = isset( $_REQUEST['ignore_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_action'] ) ) : ''; + $type = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : ''; + $siteid = get_current_blog_id(); + + // Capability check: mirrors the REST /dismiss-issue endpoint. + $first_id = reset( $ids ); + $valid_table = edac_get_valid_table_name( $table_name ); + if ( ! $first_id || ! $valid_table ) { + wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); + } + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup. + $post_id = (int) $wpdb->get_var( + $wpdb->prepare( 'SELECT postid FROM %i WHERE id = %d', $valid_table, $first_id ) + ); + if ( ! ( $post_id > 0 && current_user_can( 'edit_post', $post_id ) ) ) { + wp_send_json_error( new \WP_Error( '-5', __( 'Permission Denied', 'accessibility-checker' ) ) ); + } + $ignre = ( 'enable' === $action ) ? 1 : 0; $ignre_user = ( 'enable' === $action ) ? get_current_user_id() : null; $ignre_user_info = ( 'enable' === $action ) ? get_userdata( $ignre_user ) : ''; From e484363427a0abaa6db9f07e53005918ec3a60f3 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 20 May 2026 13:51:48 -0400 Subject: [PATCH 2/8] Expand add_ignore capability check to all affected posts The initial fix only checked the first issue ID. A mixed-ID batch or a largeBatch request (which updates by object string site-wide) could still affect posts the user cannot edit. Now resolves all distinct postids for the full batch (or all posts sharing the same object in largeBatch mode) and gates on edit_post for every one before any UPDATE runs. Co-Authored-By: Claude Sonnet 4.6 --- admin/class-ajax.php | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/admin/class-ajax.php b/admin/class-ajax.php index ca2e37b5b..09c04c8a9 100644 --- a/admin/class-ajax.php +++ b/admin/class-ajax.php @@ -831,18 +831,35 @@ function ( $value ) { $type = isset( $_REQUEST['ignore_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ignore_type'] ) ) : ''; $siteid = get_current_blog_id(); - // Capability check: mirrors the REST /dismiss-issue endpoint. + // Capability check: verify edit_post on every post affected by this request. $first_id = reset( $ids ); $valid_table = edac_get_valid_table_name( $table_name ); + if ( ! $first_id || ! $valid_table ) { wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); } - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup. - $post_id = (int) $wpdb->get_var( - $wpdb->prepare( 'SELECT postid FROM %i WHERE id = %d', $valid_table, $first_id ) - ); - if ( ! ( $post_id > 0 && current_user_can( 'edit_post', $post_id ) ) ) { - wp_send_json_error( new \WP_Error( '-5', __( 'Permission Denied', 'accessibility-checker' ) ) ); + + if ( isset( $_REQUEST['largeBatch'] ) && 'true' === $_REQUEST['largeBatch'] ) { + // largeBatch updates every row sharing the same object string across the site; + // collect all distinct postids that would be affected and check each one. + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup. + $batch_object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $valid_table, $first_id ) ); + if ( ! $batch_object ) { + wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); + } + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup. + $affected_post_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT postid FROM %i WHERE siteid = %d AND object = %s', $valid_table, $siteid, $batch_object ) ); + } else { + // Small batch: look up the post for every supplied ID in one query. + $id_placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) ); + $query_args = array_merge( [ $valid_table ], $ids ); + $affected_post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT postid FROM %i WHERE id IN ({$id_placeholders})", $query_args ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- Permission check requires direct lookup. + } + + foreach ( (array) $affected_post_ids as $affected_post_id ) { + if ( ! current_user_can( 'edit_post', (int) $affected_post_id ) ) { + wp_send_json_error( new \WP_Error( '-5', __( 'Permission Denied', 'accessibility-checker' ) ) ); + } } $ignre = ( 'enable' === $action ) ? 1 : 0; From 9fb22d2ab5adcbaef743542f1e6eb965e3e33f00 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 20 May 2026 13:55:42 -0400 Subject: [PATCH 3/8] Eliminate duplicate object lookup in largeBatch capability check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SELECT object query was running twice for largeBatch requests — once during the capability check and again in the execution block. Reuse $batch_object to avoid the redundant query. Co-Authored-By: Claude Sonnet 4.6 --- admin/class-ajax.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/admin/class-ajax.php b/admin/class-ajax.php index 09c04c8a9..b52b30e19 100644 --- a/admin/class-ajax.php +++ b/admin/class-ajax.php @@ -832,8 +832,9 @@ function ( $value ) { $siteid = get_current_blog_id(); // Capability check: verify edit_post on every post affected by this request. - $first_id = reset( $ids ); - $valid_table = edac_get_valid_table_name( $table_name ); + $batch_object = null; + $first_id = reset( $ids ); + $valid_table = edac_get_valid_table_name( $table_name ); if ( ! $first_id || ! $valid_table ) { wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); @@ -876,14 +877,8 @@ function ( $value ) { // instead of IDs. It is a much less efficient query than by IDs - but many IDs run // into request size limits which caused this to not function at all. if ( isset( $_REQUEST['largeBatch'] ) && 'true' === $_REQUEST['largeBatch'] ) { - // Get the 'object' from the first id. - $first_id = $ids[0]; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to get the latest value, not a cached value. - $object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $table_name, $first_id ) ); - - if ( ! $object ) { - wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); - } + // $batch_object was already resolved during the capability check above. + $object = $batch_object; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name, caching not required for one time operation. $wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_reason = %s, ignre_global = %d WHERE siteid = %d and object = %s', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignre_reason, $ignore_global, $siteid, $object ) ); } else { From 81d3b4060b7cb1367461f00453b5b34120fd2c40 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 20 May 2026 14:02:31 -0400 Subject: [PATCH 4/8] Address CodeRabbit and Gemini review feedback - Return -2 error when $affected_post_ids is empty (stale IDs should not succeed silently) rather than letting the foreach no-op and responding success - Remove redundant (array) cast; wpdb::get_col() always returns an array Co-Authored-By: Claude Sonnet 4.6 --- admin/class-ajax.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/admin/class-ajax.php b/admin/class-ajax.php index b52b30e19..736ebfc22 100644 --- a/admin/class-ajax.php +++ b/admin/class-ajax.php @@ -857,7 +857,11 @@ function ( $value ) { $affected_post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT postid FROM %i WHERE id IN ({$id_placeholders})", $query_args ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- Permission check requires direct lookup. } - foreach ( (array) $affected_post_ids as $affected_post_id ) { + if ( empty( $affected_post_ids ) ) { + wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) ); + } + + foreach ( $affected_post_ids as $affected_post_id ) { if ( ! current_user_can( 'edit_post', (int) $affected_post_id ) ) { wp_send_json_error( new \WP_Error( '-5', __( 'Permission Denied', 'accessibility-checker' ) ) ); } From e919d7d0be16d1d0f86c1180b9dc9abef447b105 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 20 May 2026 14:48:15 -0400 Subject: [PATCH 5/8] Bump version to 1.42.1 and update changelog --- accessibility-checker.php | 4 ++-- changelog.txt | 3 +++ package.json | 2 +- readme.txt | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/accessibility-checker.php b/accessibility-checker.php index dd55a623c..deebb0ff7 100755 --- a/accessibility-checker.php +++ b/accessibility-checker.php @@ -10,7 +10,7 @@ * Plugin Name: Accessibility Checker * Plugin URI: https://a11ychecker.com * Description: Audit and check your website for accessibility before you hit publish. In-post accessibility scanner and guidance. - * Version: 1.42.0 + * Version: 1.42.1 * Requires PHP: 7.4 * Author: Equalize Digital * Author URI: https://equalizedigital.com @@ -36,7 +36,7 @@ // Current plugin version. if ( ! defined( 'EDAC_VERSION' ) ) { - define( 'EDAC_VERSION', '1.42.0' ); + define( 'EDAC_VERSION', '1.42.1' ); } // Current database version. diff --git a/changelog.txt b/changelog.txt index 4b651a8d1..e1d8e1443 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,8 @@ *** Accessibility Checker *** +2026-05-20 - version 1.42.1 +* Updated - improved permission handling for the ignore functionality. + 2026-05-18 - version 1.42.0 * Updated - add support for role="none" in several of the link and image alt related rules. * Updated - added support for named anchors as jump links. diff --git a/package.json b/package.json index 1b7a9ca34..779d04d2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accessibility-checker", - "version": "1.42.0", + "version": "1.42.1", "description": "Audit and check your website for accessibility before you hit publish. In-post accessibility scanner and guidance.", "author": "Equalize Digital", "license": "GPL-2.0+", diff --git a/readme.txt b/readme.txt index 31e21529b..1087f17d6 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: equalizedigital, alh0319, stevejonesdev Tags: accessibility, EAA, WCAG, ADA, WP accessibility, accessibility scanner Requires at least: 6.7 Tested up to: 7.0 -Stable tag: 1.42.0 +Stable tag: 1.42.1 Requires PHP: 7.4 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -279,6 +279,9 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == +2026-05-20 - version 1.42.1 +* Updated - improved permission handling for the ignore functionality. + 2026-05-18 - version 1.42.0 * Updated - add support for role="none" in several of the link and image alt related rules. * Updated - added support for named anchors as jump links. From 719272cadee113835cbc723d5da494b387be5312 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 20 May 2026 15:00:38 -0400 Subject: [PATCH 6/8] Update changelog to standardize terminology for ignore feature --- changelog.txt | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index e1d8e1443..2da73846f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,7 @@ *** Accessibility Checker *** 2026-05-20 - version 1.42.1 -* Updated - improved permission handling for the ignore functionality. +* Updated - improved permission handling for the ignore feature. 2026-05-18 - version 1.42.0 * Updated - add support for role="none" in several of the link and image alt related rules. diff --git a/readme.txt b/readme.txt index 1087f17d6..cd832946f 100644 --- a/readme.txt +++ b/readme.txt @@ -280,7 +280,7 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == 2026-05-20 - version 1.42.1 -* Updated - improved permission handling for the ignore functionality. +* Updated - improved permission handling for the ignore feature. 2026-05-18 - version 1.42.0 * Updated - add support for role="none" in several of the link and image alt related rules. From ff6109e05d06bb60deb65fe4c66ac799a670cc9d Mon Sep 17 00:00:00 2001 From: pattonwebz <223556219+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 20:17:02 +0100 Subject: [PATCH 7/8] Tests: use real WP_Screen in screen mocks for WP 7.0 compatibility WP 7.0 added an `instanceof WP_Screen` guard to `get_current_screen()` (wp-admin/includes/screen.php). The anonymous-class mocks assigned to $GLOBALS['current_screen'] in EnqueueAdminTest and MetaBoxesTest no longer satisfy that check, so `Helpers::is_block_editor()` short-circuits to false and several enqueue/metabox tests fail. `WP_Screen` is final, so we can't subclass it. Use `set_current_screen()` to install a real screen and toggle the public `is_block_editor` property to simulate block-editor vs classic-editor contexts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/phpunit/Admin/EnqueueAdminTest.php | 40 +++--------------------- tests/phpunit/Admin/MetaBoxesTest.php | 31 +++--------------- 2 files changed, 10 insertions(+), 61 deletions(-) diff --git a/tests/phpunit/Admin/EnqueueAdminTest.php b/tests/phpunit/Admin/EnqueueAdminTest.php index d4ff4420f..a60c13829 100644 --- a/tests/phpunit/Admin/EnqueueAdminTest.php +++ b/tests/phpunit/Admin/EnqueueAdminTest.php @@ -375,40 +375,10 @@ public function testSrOnlyFormatDoesNotEnqueueOnOtherAdminPages() { * @param bool $is_block_editor Whether the screen should behave as block editor. */ private function set_mock_screen( bool $is_block_editor ): void { - $GLOBALS['current_screen'] = new class( $is_block_editor ) { - /** - * True or false whether the screen is block editor. - * - * @var bool - */ - private $is_block_editor; - - /** - * Constructor. - * - * @param bool $is_block_editor Whether the screen should behave as block editor. - */ - public function __construct( bool $is_block_editor ) { - $this->is_block_editor = $is_block_editor; - } - - /** - * Mock is_block_editor method. - * - * @return bool - */ - public function is_block_editor(): bool { - return $this->is_block_editor; - } - - /** - * Mock in_admin method. - * - * @return bool - */ - public function in_admin(): bool { - return true; - } - }; + // As of WP 7.0, get_current_screen() requires an actual WP_Screen + // instance (instanceof check), so substituting an anonymous class no + // longer works. Use a real screen and toggle the public property. + set_current_screen( 'post' ); + $GLOBALS['current_screen']->is_block_editor = $is_block_editor; } } diff --git a/tests/phpunit/Admin/MetaBoxesTest.php b/tests/phpunit/Admin/MetaBoxesTest.php index d5c07adb3..635b6857c 100644 --- a/tests/phpunit/Admin/MetaBoxesTest.php +++ b/tests/phpunit/Admin/MetaBoxesTest.php @@ -128,32 +128,11 @@ public function test_register_meta_boxes_keeps_classic_editor_when_setting_disab * @param bool $is_block_editor Whether the screen should behave as block editor. */ private function set_mock_screen( bool $is_block_editor ): void { - $GLOBALS['current_screen'] = new class( $is_block_editor ) { - /** - * True or false whether the screen is block editor. - * - * @var bool - */ - private $is_block_editor; - - /** - * Constructor. - * - * @param bool $is_block_editor Whether the screen should behave as block editor. - */ - public function __construct( bool $is_block_editor ) { - $this->is_block_editor = $is_block_editor; - } - - /** - * Mock is_block_editor method. - * - * @return bool - */ - public function is_block_editor(): bool { - return $this->is_block_editor; - } - }; + // As of WP 7.0, get_current_screen() requires an actual WP_Screen + // instance, so we must use a real screen and toggle its public + // is_block_editor property rather than substituting an anonymous class. + set_current_screen( 'post' ); + $GLOBALS['current_screen']->is_block_editor = $is_block_editor; } /** From 2a7b569437d9238e581557cc0816120013a81db1 Mon Sep 17 00:00:00 2001 From: pattonwebz Date: Wed, 20 May 2026 20:25:45 +0100 Subject: [PATCH 8/8] Tests: use WP_Screen::get() over set_current_screen() to avoid global side effects set_current_screen() sets $hook_suffix, $typenow, and $taxnow, and fires the current_screen action. Since tearDown() only unsets $GLOBALS['current_screen'], those globals were left polluted between tests. WP_Screen::get() returns a proper WP_Screen instance (satisfying the WP 7.0 instanceof check) without any of those side effects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/phpunit/Admin/EnqueueAdminTest.php | 7 ++++--- tests/phpunit/Admin/MetaBoxesTest.php | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/Admin/EnqueueAdminTest.php b/tests/phpunit/Admin/EnqueueAdminTest.php index a60c13829..946529035 100644 --- a/tests/phpunit/Admin/EnqueueAdminTest.php +++ b/tests/phpunit/Admin/EnqueueAdminTest.php @@ -376,9 +376,10 @@ public function testSrOnlyFormatDoesNotEnqueueOnOtherAdminPages() { */ private function set_mock_screen( bool $is_block_editor ): void { // As of WP 7.0, get_current_screen() requires an actual WP_Screen - // instance (instanceof check), so substituting an anonymous class no - // longer works. Use a real screen and toggle the public property. - set_current_screen( 'post' ); + // instance. Use WP_Screen::get() to obtain one without the side effects + // of set_current_screen() (e.g. setting $hook_suffix, $typenow, firing + // the current_screen action). + $GLOBALS['current_screen'] = WP_Screen::get( 'post' ); $GLOBALS['current_screen']->is_block_editor = $is_block_editor; } } diff --git a/tests/phpunit/Admin/MetaBoxesTest.php b/tests/phpunit/Admin/MetaBoxesTest.php index 635b6857c..456e22812 100644 --- a/tests/phpunit/Admin/MetaBoxesTest.php +++ b/tests/phpunit/Admin/MetaBoxesTest.php @@ -129,9 +129,10 @@ public function test_register_meta_boxes_keeps_classic_editor_when_setting_disab */ private function set_mock_screen( bool $is_block_editor ): void { // As of WP 7.0, get_current_screen() requires an actual WP_Screen - // instance, so we must use a real screen and toggle its public - // is_block_editor property rather than substituting an anonymous class. - set_current_screen( 'post' ); + // instance. Use WP_Screen::get() to obtain one without the side effects + // of set_current_screen() (e.g. setting $hook_suffix, $typenow, firing + // the current_screen action). + $GLOBALS['current_screen'] = WP_Screen::get( 'post' ); $GLOBALS['current_screen']->is_block_editor = $is_block_editor; }