Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion admin/class-enqueue-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() {
[
'postID' => $post_id,
'nonce' => wp_create_nonce( 'ajax-nonce' ),
'edacApiUrl' => esc_url_raw( rest_url() . 'accessibility-checker/v1' ),
'edacApiUrl' => esc_url_raw( rest_url( 'accessibility-checker/v1' ) ),
'fixesRestUrl' => esc_url_raw( rest_url( 'edac/v1' ) ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'proUrl' => esc_url_raw( edac_generate_link_type( [ 'utm_content' => '__name__' ] ) ),
'hasDismissEndpoint' => method_exists( \EDAC\Inc\REST_Api::class, 'dismiss_issue' ),
Expand Down
2 changes: 2 additions & 0 deletions includes/classes/class-enqueue-frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ public static function maybe_enqueue_frontend_highlighter() {
'isPro' => edac_is_pro(),
'userCanEdit' => current_user_can( 'edit_post', $post_id ),
'edacUrl' => esc_url_raw( get_site_url() ),
'restUrl' => esc_url_raw( rest_url( 'accessibility-checker/v1' ) ),
'fixesRestUrl' => esc_url_raw( rest_url( 'edac/v1' ) ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'loggedIn' => is_user_logged_in(),
'appCssUrl' => EDAC_PLUGIN_URL . 'build/css/frontendHighlighterApp.css?ver=' . EDAC_VERSION,
Expand Down
4 changes: 3 additions & 1 deletion src/common/saveFixSettingsRest.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export const saveFixSettings = ( fixSettingsContainer ) => {
liveRegion.innerText = __( 'Saving...', 'accessibility-checker' );
}

const fixesRestUrl = window.edacFrontendHighlighterApp?.fixesRestUrl ?? window.edac_script_vars?.fixesRestUrl;

// make a rest call to save the settings
fetch( '/wp-json/edac/v1/fixes/update/', {
fetch( `${ fixesRestUrl }/fixes/update/`, {
Comment on lines +44 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If fixesRestUrl is undefined or null (for example, if the script is loaded in a context where the localized variables are missing), the fetch call will attempt to request a relative URL starting with undefined/fixes/update/. Adding a guard clause to handle this scenario gracefully prevents broken network requests and improves robustness.

	const fixesRestUrl = window.edacFrontendHighlighterApp?.fixesRestUrl ?? window.edac_script_vars?.fixesRestUrl;

	if ( ! fixesRestUrl ) {
		fixSettingsContainer.classList.remove( 'edac-fix-settings--saving' );
		fixButtons.forEach( ( button ) => {
			button.disabled = false;
		} );
		fixSettingsContainer.classList.add( 'edac-fix-settings--saved--error' );
		if ( liveRegion ) {
			liveRegion.innerText = __( 'Saving failed: Missing REST API URL.', 'accessibility-checker' );
		}
		return;
	}

	// make a rest call to save the settings
	fetch( fixesRestUrl + '/fixes/update/', {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce8674d — added guard clause / switched to window. prefix throughout.

method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
6 changes: 3 additions & 3 deletions src/frontendHighlighterApp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1863,7 +1863,7 @@ class AccessibilityCheckerHighlight {

saveScanResults( postId, nonce, violations, densityMetrics ) {
const self = this;
return fetch( '/wp-json/accessibility-checker/v1/post-scan-results/' + postId, {
return fetch( `${ edacFrontendHighlighterApp.restUrl }/post-scan-results/${ postId }`, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If window.edacFrontendHighlighterApp is defined but restUrl is missing or undefined, the fetch call will attempt to request a relative URL starting with undefined/post-scan-results/.... Adding a check to ensure restUrl is present before making the API call prevents broken network requests.

		const restUrl = window.edacFrontendHighlighterApp?.restUrl;
		if ( ! restUrl ) {
			return Promise.reject( new Error( 'Missing REST API URL.' ) );
		}
		return fetch( restUrl + '/post-scan-results/' + postId, {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce8674d — added guard clause / switched to window. prefix throughout.

method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -1943,7 +1943,7 @@ class AccessibilityCheckerHighlight {
}

// Validate required parameters
if ( ! edacFrontendHighlighterApp?.edacUrl || ! edacFrontendHighlighterApp?.postID ) {
if ( ! edacFrontendHighlighterApp?.restUrl || ! edacFrontendHighlighterApp?.postID ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing edacFrontendHighlighterApp directly can throw a ReferenceError at runtime if the variable is completely undeclared (e.g., if the script is loaded in a context where the localization didn't run). Accessing it via window.edacFrontendHighlighterApp is safer as it will evaluate to undefined instead of throwing an error.

Suggested change
if ( ! edacFrontendHighlighterApp?.restUrl || ! edacFrontendHighlighterApp?.postID ) {
if ( ! window.edacFrontendHighlighterApp?.restUrl || ! window.edacFrontendHighlighterApp?.postID ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce8674d — added guard clause / switched to window. prefix throughout.

const summary = document.querySelector( '.edac-highlight-panel-controls-summary' );
if ( summary ) {
summary.textContent = __( 'Error: Missing required parameters.', 'accessibility-checker' );
Expand All @@ -1956,7 +1956,7 @@ class AccessibilityCheckerHighlight {
this.clearIssuesButton.textContent = __( 'Clearing...', 'accessibility-checker' );
const summary = document.querySelector( '.edac-highlight-panel-controls-summary' );

fetch( `${ edacFrontendHighlighterApp.edacUrl }/wp-json/accessibility-checker/v1/clear-issues/${ edacFrontendHighlighterApp.postID }`, {
fetch( `${ edacFrontendHighlighterApp.restUrl }/clear-issues/${ edacFrontendHighlighterApp.postID }`, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For safety and consistency, access the localized variables via window.edacFrontendHighlighterApp to avoid potential ReferenceError at runtime.

Suggested change
fetch( `${ edacFrontendHighlighterApp.restUrl }/clear-issues/${ edacFrontendHighlighterApp.postID }`, {
fetch( window.edacFrontendHighlighterApp.restUrl + '/clear-issues/' + window.edacFrontendHighlighterApp.postID, {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce8674d — added guard clause / switched to window. prefix throughout.

method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
70 changes: 70 additions & 0 deletions tests/phpunit/Admin/EnqueueAdminTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,76 @@ public function testLocalizedProUrlUsesUnderscoreUtmContentKey() {
$this->assertStringNotContainsString( 'utm-content=__name__', $localized_data );
}

/**
* FixesRestUrl is present in the edac_script_vars localized to the admin script.
*/
public function testLocalizedAdminDataIncludesFixesRestUrl(): void {
global $wp_scripts;

$this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts();

$localized_data = (string) $wp_scripts->get_data( 'edac', 'data' );

$this->assertNotEmpty( $localized_data );
$this->assertStringContainsString( 'fixesRestUrl', $localized_data );
}

/**
* FixesRestUrl uses the edac/v1 namespace and matches rest_url().
*/
public function testAdminFixesRestUrlContainsEdacV1Namespace(): void {
global $wp_scripts;

$this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts();

$localized_data = (string) $wp_scripts->get_data( 'edac', 'data' );
$expected = rest_url( 'edac/v1' );

$this->assertStringContainsString( 'edac', $localized_data );
$this->assertStringContainsString( (string) wp_parse_url( $expected, PHP_URL_HOST ), $localized_data );
}

/**
* FixesRestUrl must be an absolute URL — a root-relative /wp-json path would break
* subdomain multisite installs by resolving to the main site instead of the subsite.
*/
public function testAdminFixesRestUrlIsAbsolute(): void {
global $wp_scripts;

$this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts();

$localized_data = (string) $wp_scripts->get_data( 'edac', 'data' );

$this->assertDoesNotMatchRegularExpression( '/"fixesRestUrl"\s*:\s*"\\\\?\/wp-json/', $localized_data );
$this->assertMatchesRegularExpression( '/"fixesRestUrl"\s*:\s*"https?/', $localized_data );
}

/**
* FixesRestUrl in edac_script_vars must follow a custom REST base prefix set via
* the rest_url_prefix filter. Verifies the URL is built with rest_url() rather than
* a hardcoded /wp-json/ string.
* Pretty permalinks are required for the prefix filter to be applied.
*/
public function testAdminFixesRestUrlRespectsCustomRestPrefix(): void {
global $wp_scripts;

update_option( 'permalink_structure', '/%postname%/' );
flush_rewrite_rules(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules

$prefix_callback = static fn() => 'custom-api';
add_filter( 'rest_url_prefix', $prefix_callback );

$this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts();

remove_filter( 'rest_url_prefix', $prefix_callback );
delete_option( 'permalink_structure' );

$localized_data = (string) $wp_scripts->get_data( 'edac', 'data' );

$this->assertStringContainsString( 'custom-api', $localized_data );
$this->assertStringNotContainsString( 'wp-json', $localized_data );
}

/**
* Test that the base script and editor script is enqueued in the editor for an existing page.
*
Expand Down
126 changes: 126 additions & 0 deletions tests/phpunit/includes/classes/EnqueueFrontendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,132 @@ public function testScannerBundleUrlIncludesVersionQueryString(): void {
$this->assertStringContainsString( 'ver=' . EDAC_VERSION, $localized_data );
}

/**
* Helper: enqueue the frontend highlighter as an admin and return the localized data string.
*
* @return string The raw JS localized-data string for edac-frontend-highlighter-app.
*/
private function enqueueAndGetLocalizedData(): string {
$admin_id = $this->factory()->user->create( [ 'role' => 'administrator' ] );
wp_set_current_user( $admin_id );

global $post;
$post = $this->factory()->post->create_and_get( [ 'post_type' => 'post' ] );

Enqueue_Frontend::maybe_enqueue_frontend_highlighter();

global $wp_scripts;
return (string) $wp_scripts->get_data( 'edac-frontend-highlighter-app', 'data' );
}

/**
* RestUrl is present in the localized data passed to the frontend highlighter script.
*/
public function testLocalizedDataIncludesRestUrl(): void {
$localized_data = $this->enqueueAndGetLocalizedData();

$this->assertNotEmpty( $localized_data );
$this->assertStringContainsString( 'restUrl', $localized_data );
}

/**
* RestUrl uses the accessibility-checker/v1 namespace and matches rest_url().
*/
public function testRestUrlMatchesRestUrlFunction(): void {
$localized_data = $this->enqueueAndGetLocalizedData();
$expected = rest_url( 'accessibility-checker/v1' );

$this->assertStringContainsString( 'accessibility-checker', $localized_data );
$this->assertStringContainsString( 'v1', $localized_data );
// The URL must be derived from rest_url(), not hardcoded — verify the host is present.
$this->assertStringContainsString( (string) wp_parse_url( $expected, PHP_URL_HOST ), $localized_data );
}

/**
* RestUrl must be an absolute URL, not a root-relative path like /wp-json/...
* A root-relative URL on a subdomain multisite would resolve to the main site.
*/
public function testRestUrlIsAbsolute(): void {
$localized_data = $this->enqueueAndGetLocalizedData();

// The value following "restUrl" must not be a bare /wp-json path.
$this->assertDoesNotMatchRegularExpression( '/"restUrl"\s*:\s*"\\\\?\/wp-json/', $localized_data );
// And the scheme must be present.
$this->assertMatchesRegularExpression( '/"restUrl"\s*:\s*"https?/', $localized_data );
}

/**
* FixesRestUrl is present in the localized data passed to the frontend highlighter script.
*/
public function testLocalizedDataIncludesFixesRestUrl(): void {
$localized_data = $this->enqueueAndGetLocalizedData();

$this->assertStringContainsString( 'fixesRestUrl', $localized_data );
}

/**
* FixesRestUrl uses the edac/v1 namespace and matches rest_url().
*/
public function testFixesRestUrlContainsEdacV1Namespace(): void {
$localized_data = $this->enqueueAndGetLocalizedData();
$expected = rest_url( 'edac/v1' );

$this->assertStringContainsString( 'edac', $localized_data );
$this->assertStringContainsString( (string) wp_parse_url( $expected, PHP_URL_HOST ), $localized_data );
}

/**
* FixesRestUrl must be an absolute URL, not a root-relative path.
*/
public function testFixesRestUrlIsAbsolute(): void {
$localized_data = $this->enqueueAndGetLocalizedData();

$this->assertDoesNotMatchRegularExpression( '/"fixesRestUrl"\s*:\s*"\\\\?\/wp-json/', $localized_data );
$this->assertMatchesRegularExpression( '/"fixesRestUrl"\s*:\s*"https?/', $localized_data );
}

/**
* RestUrl must follow a custom REST base prefix set via the rest_url_prefix filter.
* Verifies the URL is built with rest_url() rather than a hardcoded /wp-json/ string.
* Pretty permalinks are required for the prefix filter to be applied.
*/
public function testRestUrlRespectsCustomRestPrefix(): void {
update_option( 'permalink_structure', '/%postname%/' );
flush_rewrite_rules(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules

$prefix_callback = static fn() => 'custom-api';
add_filter( 'rest_url_prefix', $prefix_callback );
$this->added_filters['rest_url_prefix'] = $prefix_callback;

$localized_data = $this->enqueueAndGetLocalizedData();

remove_filter( 'rest_url_prefix', $prefix_callback );
delete_option( 'permalink_structure' );

$this->assertStringContainsString( 'custom-api', $localized_data );
$this->assertStringNotContainsString( 'wp-json', $localized_data );
}

/**
* FixesRestUrl must follow a custom REST base prefix set via the rest_url_prefix filter.
*/
public function testFixesRestUrlRespectsCustomRestPrefix(): void {
update_option( 'permalink_structure', '/%postname%/' );
flush_rewrite_rules(); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules

$prefix_callback = static fn() => 'custom-api';
add_filter( 'rest_url_prefix', $prefix_callback );
$this->added_filters['rest_url_prefix'] = $prefix_callback;

$localized_data = $this->enqueueAndGetLocalizedData();

remove_filter( 'rest_url_prefix', $prefix_callback );
delete_option( 'permalink_structure' );

$this->assertStringContainsString( 'custom-api', $localized_data );
$this->assertStringNotContainsString( 'wp-json', $localized_data );
}

/**
* Ensure the highlighter uses the filtered post ID when determining scannable post types.
*/
Expand Down
Loading