Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
1 change: 1 addition & 0 deletions includes/classes/class-enqueue-frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
41 changes: 32 additions & 9 deletions includes/classes/class-rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
},
]
);
Expand All @@ -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' );
},
]
);
Expand All @@ -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( 'edit_posts' );
},
]
);
Expand All @@ -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' );
},
]
);
Expand All @@ -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' );
},
]
);
Expand All @@ -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 );
},
]
);
Expand All @@ -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.31.0
*
* @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.
}
Comment thread
pattonwebz marked this conversation as resolved.

Comment thread
pattonwebz marked this conversation as resolved.
/**
* 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
*/
Expand Down
8 changes: 4 additions & 4 deletions src/frontendHighlighterApp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
? `<button id="edac-highlight-clear-issues" class="edac-highlight-clear-issues">${ __( 'Clear Issues', 'accessibility-checker' ) }</button>`
: '';

const rescanButton = isLoggedInUser
const rescanButton = userCanEdit
? `<button id="edac-highlight-rescan" class="edac-highlight-rescan">${ __( 'Rescan This Page', 'accessibility-checker' ) }</button>`
: '';

Expand All @@ -416,7 +416,7 @@ class AccessibilityCheckerHighlight {
<button id="edac-highlight-panel-controls-close" class="edac-highlight-panel-controls-close" aria-label="Close">×</button>
<div class="edac-highlight-panel-controls-title">Accessibility Checker</div>
<div class="edac-highlight-panel-controls-summary">Loading...</div>
<div class="edac-highlight-panel-controls-buttons ${ ! isLoggedInUser ? ' single_button' : '' }">
<div class="edac-highlight-panel-controls-buttons ${ ! userCanEdit ? ' single_button' : '' }">
<div>
<button id="edac-highlight-previous" disabled="true"><span aria-hidden="true">« </span>Previous</button>
<button id="edac-highlight-next" disabled="true">Next<span aria-hidden="true"> »</span></button><br />
Expand Down
218 changes: 218 additions & 0 deletions tests/phpunit/includes/classes/RestApiEndpointsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
<?php
/**
* REST API endpoints behavior tests.
*
* @package Accessibility_Checker
*/

/**
* Test class for REST API endpoints.
*
* @group rest
*/
class RestApiEndpointsTest extends WP_UnitTestCase {
/**
* Admin user ID.
*
* @var int
*/
protected static $admin_id;

/**
* Limited user ID.
*
* @var int
*/
protected static $limited_id;

/**
* Post ID used for tests.
*
* @var int
*/
protected static $post_id;

/**
* REST server instance for dispatching requests.
*
* @var WP_REST_Server|null
*/
private $server;

/**
* Set up before each test.
*
* @return void
*/
protected function setUp(): void {
parent::setUp();
// Initialize REST routes for each test.
do_action( 'init' );
do_action( 'rest_api_init' );
$this->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' => '<main><h1>Title</h1><p>Img without alt <img src="/wp-includes/images/media/default.png"></p></main>',
]
);
}

/**
* 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' => '<img src="/wp-includes/images/media/default.png">',
'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' => '<img>',
],
]
);
$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'] );
}
}