diff --git a/accessibility-checker.php b/accessibility-checker.php index 0f1e31bf7..b7c745b88 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.30.0 + * Version: 1.30.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.30.0' ); + define( 'EDAC_VERSION', '1.30.1' ); } // Current database version. @@ -122,7 +122,7 @@ function edac_register_rules() { // Use the new class-based rules system. $default_rules = \EqualizeDigital\AccessibilityChecker\Rules\RuleRegistry::load_rules(); - + /** * Filter the default rules. * diff --git a/admin/class-welcome-page.php b/admin/class-welcome-page.php index 4679a90d6..6f541b603 100644 --- a/admin/class-welcome-page.php +++ b/admin/class-welcome-page.php @@ -40,9 +40,11 @@ public static function render_summary() {

- + + + diff --git a/includes/classes/class-enqueue-frontend.php b/includes/classes/class-enqueue-frontend.php index 60931d338..ce56415de 100644 --- a/includes/classes/class-enqueue-frontend.php +++ b/includes/classes/class-enqueue-frontend.php @@ -101,6 +101,7 @@ public static function maybe_enqueue_frontend_highlighter() { 'nonce' => wp_create_nonce( 'ajax-nonce' ), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'userCanFix' => current_user_can( apply_filters( 'edac_filter_settings_capability', 'manage_options' ) ), + 'userCanEdit' => current_user_can( 'edit_post', $post_id ), 'edacUrl' => esc_url_raw( get_site_url() ), 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'loggedIn' => is_user_logged_in(), diff --git a/includes/classes/class-rest-api.php b/includes/classes/class-rest-api.php index 0186c850d..d1f1a0c4d 100644 --- a/includes/classes/class-rest-api.php +++ b/includes/classes/class-rest-api.php @@ -79,13 +79,15 @@ function () use ( $ns, $version ) { 'callback' => [ $this, 'set_post_scan_results' ], 'args' => [ 'id' => [ + 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, + 'sanitize_callback' => 'absint', ], ], - 'permission_callback' => function () { - return current_user_can( 'edit_posts' ); + 'permission_callback' => function ( $request ) { + return $this->user_can_edit_passed_post_id( $request ); }, ] ); @@ -102,7 +104,7 @@ function () use ( $ns, $version ) { 'methods' => 'GET', 'callback' => [ $this, 'get_scans_stats' ], 'permission_callback' => function () { - return current_user_can( 'read' ); // able to access the admin dashboard. + return current_user_can( 'edit_posts' ); }, ] ); @@ -119,7 +121,7 @@ function () use ( $ns, $version ) { 'methods' => 'POST', 'callback' => [ $this, 'clear_cached_scans_stats' ], 'permission_callback' => function () { - return current_user_can( 'read' ); // able to access the admin dashboard. + return current_user_can( 'publish_posts' ); }, ] ); @@ -136,7 +138,7 @@ function () use ( $ns, $version ) { 'methods' => 'GET', 'callback' => [ $this, 'get_scans_stats_by_post_type' ], 'permission_callback' => function () { - return current_user_can( 'read' ); // able to access the admin dashboard. + return current_user_can( 'edit_posts' ); }, ] ); @@ -153,7 +155,7 @@ function () use ( $ns, $version ) { 'methods' => 'GET', 'callback' => [ $this, 'get_scans_stats_by_post_types' ], 'permission_callback' => function () { - return current_user_can( 'read' ); // able to access the admin dashboard. + return current_user_can( 'edit_posts' ); }, ] ); @@ -171,13 +173,15 @@ function () use ( $ns, $version ) { 'callback' => [ $this, 'clear_issues_for_post' ], 'args' => [ 'id' => [ + 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ); }, + 'sanitize_callback' => 'absint', ], ], - 'permission_callback' => function () { - return current_user_can( 'edit_posts' ); + 'permission_callback' => function ( $request ) { + return $this->user_can_edit_passed_post_id( $request ); }, ] ); @@ -203,10 +207,29 @@ function () use ( $ns, $version ) { ); } + /** + * Check if the user can edit a post. + * + * This is a permission callback to replace several places where we check if the user can edit a post. + * + * @since 1.30.1 + * + * @param \WP_REST_Request $request The request object passed from the REST call. This should contain the 'id' of the post to check permissions for. + * + * @return bool|\WP_Error + */ + public function user_can_edit_passed_post_id( $request ) { + if ( ! isset( $request['id'] ) ) { + return new \WP_Error( 'rest_post_invalid_id', __( 'A required parameter is missing.', 'accessibility-checker' ), [ 'status' => 400 ] ); + } + $post_id = (int) $request['id']; + return current_user_can( 'edit_post', $post_id ); // able to edit the post. + } + /** * REST handler to clear issues results for a given post ID. * - * @param WP_REST_Request $request The request passed from the REST call. + * @param \WP_REST_Request $request The request passed from the REST call. * * @return \WP_REST_Response */ diff --git a/package.json b/package.json index de8ff35bd..c7a7ec7ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "accessibility-checker", - "version": "1.30.0", + "version": "1.30.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 7098b2d1d..b552feb86 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: equalizedigital, alh0319, stevejonesdev Tags: accessibility, accessible, wcag, ada, WP accessibility Requires at least: 6.6 Tested up to: 6.8 -Stable tag: 1.30.0 +Stable tag: 1.30.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -211,6 +211,10 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == += 1.30.1 = +* Improved: The rescan and clear buttons in the frontend highlighter are now only shown when they can be used. +* Improved: Issue saving and clearing now has more robust capability checking. + = 1.30.0 = * Added: Ability to clear issues on a post or page from the frontend highlighter. * Added: More accessibility checker details in the site health check. diff --git a/src/frontendHighlighterApp/index.js b/src/frontendHighlighterApp/index.js index f4934c5ce..10795424e 100644 --- a/src/frontendHighlighterApp/index.js +++ b/src/frontendHighlighterApp/index.js @@ -394,12 +394,12 @@ class AccessibilityCheckerHighlight { addHighlightPanel() { const widgetPosition = edacFrontendHighlighterApp?.widgetPosition || 'right'; - const isLoggedInUser = edacFrontendHighlighterApp && edacFrontendHighlighterApp?.loggedIn; - const clearButtonMarkup = isLoggedInUser + const userCanEdit = edacFrontendHighlighterApp && edacFrontendHighlighterApp?.userCanEdit && edacFrontendHighlighterApp?.loggedIn; + const clearButtonMarkup = userCanEdit ? `` : ''; - const rescanButton = isLoggedInUser + const rescanButton = userCanEdit ? `` : ''; @@ -416,7 +416,7 @@ class AccessibilityCheckerHighlight {

Accessibility Checker
Loading...
-
+

diff --git a/tests/phpunit/includes/classes/RestApiEndpointsTest.php b/tests/phpunit/includes/classes/RestApiEndpointsTest.php new file mode 100644 index 000000000..cfb30d599 --- /dev/null +++ b/tests/phpunit/includes/classes/RestApiEndpointsTest.php @@ -0,0 +1,218 @@ +server = rest_get_server(); + } + + /** + * Clean up after each test. + * + * @return void + */ + protected function tearDown(): void { + // Reset current user between tests. + wp_set_current_user( 0 ); + parent::tearDown(); + } + + /** + * Create shared fixtures for this test class. + * + * @param WP_UnitTest_Factory $factory Factory instance. + * @return void + */ + public static function wpSetUpBeforeClass( $factory ) { + // Ensure posts are scannable by plugin. + update_option( 'edac_post_types', [ 'post' ] ); + + // Ensure plugin DB table exists for tests (normally created via admin_init). + ( new \EDAC\Admin\Update_Database() )->edac_update_database(); + + self::$admin_id = $factory->user->create( [ 'role' => 'administrator' ] ); + self::$limited_id = $factory->user->create( [ 'role' => 'subscriber' ] ); + // Give limited user edit_posts but not edit_others_posts so they cannot edit this post. + $user = new WP_User( self::$limited_id ); + $user->add_cap( 'edit_posts' ); + + self::$post_id = $factory->post->create( + [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_author' => self::$admin_id, + 'post_title' => 'EDAC PHPUnit Post', + 'post_content' => '

Title

Img without alt

', + ] + ); + } + + /** + * Verify permissions for saving post scan results. + * + * @return void + */ + public function test_rest_post_scan_results_permissions() { + $this->assertNotNull( $this->server ); + + // Minimal violations payload similar to scanner output. + $violations = [ + [ + 'ruleId' => 'image-alt', + 'html' => '', + 'impact' => 'error', + 'landmark' => null, + ], + ]; + + // Admin can POST results for the post. + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/accessibility-checker/v1/post-scan-results/' . self::$post_id ); + $request->set_param( 'id', self::$post_id ); + $request->set_param( 'violations', $violations ); + $response = $this->server->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Admin should be allowed to save scan results.' ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'success', $data ); + $this->assertTrue( $data['success'] ); + $this->assertArrayHasKey( 'id', $data ); + $this->assertSame( self::$post_id, $data['id'] ); + + // Limited user cannot POST results for the admin-owned post. + wp_set_current_user( self::$limited_id ); + $request2 = new WP_REST_Request( 'POST', '/accessibility-checker/v1/post-scan-results/' . self::$post_id ); + $request2->set_param( 'id', self::$post_id ); + $request2->set_param( 'violations', $violations ); + $response2 = $this->server->dispatch( $request2 ); + $this->assertSame( 403, $response2->get_status(), 'Limited user must not be allowed to save scan results for another user\'s post.' ); + } + + /** + * Verify permissions for clearing issues for a post. + * + * @return void + */ + public function test_rest_clear_issues_permissions() { + $this->assertNotNull( $this->server ); + + // Admin can clear issues. + wp_set_current_user( self::$admin_id ); + $r1 = new WP_REST_Request( 'POST', '/accessibility-checker/v1/clear-issues/' . self::$post_id ); + $r1->set_param( 'id', self::$post_id ); + $r1->set_body( wp_json_encode( [ 'flush' => true ] ) ); + $r1->set_header( 'Content-Type', 'application/json' ); + $resp1 = $this->server->dispatch( $r1 ); + $this->assertSame( 200, $resp1->get_status(), 'Admin should be allowed to clear issues.' ); + $body1 = $resp1->get_data(); + $this->assertIsArray( $body1 ); + $this->assertArrayHasKey( 'success', $body1 ); + $this->assertTrue( $body1['success'] ); + $this->assertArrayHasKey( 'id', $body1 ); + $this->assertSame( self::$post_id, $body1['id'] ); + $this->assertArrayHasKey( 'flushed', $body1 ); + $this->assertTrue( $body1['flushed'] ); + + // Limited user cannot clear issues for a post they cannot edit. + wp_set_current_user( self::$limited_id ); + $r2 = new WP_REST_Request( 'POST', '/accessibility-checker/v1/clear-issues/' . self::$post_id ); + $r2->set_param( 'id', self::$post_id ); + $r2->set_body( wp_json_encode( [ 'flush' => true ] ) ); + $r2->set_header( 'Content-Type', 'application/json' ); + $resp2 = $this->server->dispatch( $r2 ); + $this->assertSame( 403, $resp2->get_status(), 'Limited user must not be allowed to clear issues.' ); + } + + /** + * Verify that a limited user can manage their own post. + * + * @return void + */ + public function test_limited_user_can_manage_own_post() { + wp_set_current_user( self::$limited_id ); + $own_post_id = self::factory()->post->create( + [ + 'post_type' => 'post', + 'post_status' => 'draft', + 'post_author' => self::$limited_id, + ] + ); + + // Save scan results. + $req1 = new WP_REST_Request( 'POST', '/accessibility-checker/v1/post-scan-results/' . $own_post_id ); + $req1->set_param( 'id', $own_post_id ); + $req1->set_param( + 'violations', + [ + [ + 'ruleId' => 'image-alt', + 'html' => '', + ], + ] + ); + $resp1 = $this->server->dispatch( $req1 ); + $this->assertSame( 200, $resp1->get_status() ); + $data1 = $resp1->get_data(); + $this->assertIsArray( $data1 ); + $this->assertArrayHasKey( 'success', $data1 ); + $this->assertTrue( $data1['success'] ); + + // Clear issues. + $req2 = new WP_REST_Request( 'POST', '/accessibility-checker/v1/clear-issues/' . $own_post_id ); + $req2->set_param( 'id', $own_post_id ); + $req2->set_body( wp_json_encode( [ 'flush' => true ] ) ); + $req2->set_header( 'Content-Type', 'application/json' ); + $resp2 = $this->server->dispatch( $req2 ); + $this->assertSame( 200, $resp2->get_status() ); + $data2 = $resp2->get_data(); + $this->assertIsArray( $data2 ); + $this->assertArrayHasKey( 'success', $data2 ); + $this->assertTrue( $data2['success'] ); + } +}