diff --git a/admin/class-enqueue-admin.php b/admin/class-enqueue-admin.php index 46eb51590..fcc888026 100644 --- a/admin/class-enqueue-admin.php +++ b/admin/class-enqueue-admin.php @@ -78,6 +78,11 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() { global $post; $post_id = is_object( $post ) ? $post->ID : null; + + // On a latest-posts homepage the global $post is the first blog post, not the page; + // let extensions supply the correct ID (e.g. a Pro virtual-page ID). + $post_id = apply_filters( 'edac_filter_admin_post_id', $post_id ); + wp_enqueue_script( 'edac', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/admin.bundle.js', [ 'jquery' ], EDAC_VERSION, false ); wp_set_script_translations( 'edac', 'accessibility-checker', plugin_dir_path( EDAC_PLUGIN_FILE ) . 'languages' ); @@ -99,8 +104,9 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() { if ( 'post.php' === $pagenow || 'post-new.php' === $pagenow ) { - // Is this posttype setup to be checked? - $active = $is_scannable_post; + // Base the scannable check on the filtered $post_id, not the original global $post. + $filtered_post_type = $post_id ? get_post_type( $post_id ) : false; + $active = $filtered_post_type && is_array( $post_types ) && in_array( $filtered_post_type, $post_types, true ); $pro = defined( 'EDACP_VERSION' ) && EDAC_KEY_VALID; @@ -113,8 +119,15 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() { wp_enqueue_script( 'edac-editor-app', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/editorApp.bundle.js', false, EDAC_VERSION, false ); wp_set_script_translations( 'edac-editor-app', 'accessibility-checker', plugin_dir_path( EDAC_PLUGIN_FILE ) . 'languages' ); - // If this is the frontpage or homepage, preview URLs won't work. Use the live URL. - if ( (int) get_option( 'page_on_front' ) === $post_id || (int) get_option( 'page_for_posts' ) === $post_id ) { + // Preview URLs don't work for the homepage. On a latest-posts homepage (including the + // show_on_front=page fallback with no static front page) use the live home URL instead. + $show_on_front = get_option( 'show_on_front', 'posts' ); + $is_latest_posts_home = ( 'posts' === $show_on_front || ( 'page' === $show_on_front && ! get_option( 'page_on_front' ) ) ) + && apply_filters( 'edac_filter_post_is_latest_posts_home', false, $post_id ); + + if ( $is_latest_posts_home ) { + $scan_url = add_query_arg( 'edac_pageScanner', 1, trailingslashit( get_home_url() ) ); + } elseif ( (int) get_option( 'page_on_front' ) === $post_id || (int) get_option( 'page_for_posts' ) === $post_id ) { $scan_url = add_query_arg( 'edac_pageScanner', 1, get_permalink( $post_id ) ); } else { $post_view_link = apply_filters( diff --git a/includes/classes/class-enqueue-frontend.php b/includes/classes/class-enqueue-frontend.php index ace4787f3..a2f39efda 100644 --- a/includes/classes/class-enqueue-frontend.php +++ b/includes/classes/class-enqueue-frontend.php @@ -102,7 +102,14 @@ public static function maybe_enqueue_frontend_highlighter() { // Don't load on the frontend if we don't have a post to work with. global $post; - $post_id = apply_filters( 'edac_filter_frontend_highlight_post_id', is_object( $post ) ? $post->ID : null ); + + // On a latest-posts homepage the global $post is the first blog post, so using its ID + // would misattribute results; pass null and let the filter supply an ID (Pro) or bail. + $default_post_id = ( is_home() && is_front_page() ) + ? null + : ( is_object( $post ) ? $post->ID : null ); + + $post_id = apply_filters( 'edac_filter_frontend_highlight_post_id', $default_post_id ); if ( null === $post_id ) { return; diff --git a/tests/phpunit/Admin/EnqueueAdminTest.php b/tests/phpunit/Admin/EnqueueAdminTest.php index 09186d0be..b08173897 100644 --- a/tests/phpunit/Admin/EnqueueAdminTest.php +++ b/tests/phpunit/Admin/EnqueueAdminTest.php @@ -461,6 +461,147 @@ public function testSrOnlyFormatDoesNotEnqueueOnOtherAdminPages() { } + /** + * Test that edac_filter_admin_post_id overrides the post ID localized into edac_script_vars. + * + * @return void + */ + public function testAdminPostIdFilterOverridesLocalizedPostId() { + global $post, $pagenow, $wp_scripts; + + $original_post = $this->factory()->post->create_and_get(); + $alternate_post = $this->factory()->post->create_and_get(); + $post = $original_post; + $pagenow = 'post.php'; + + $filter_callback = static function () use ( $alternate_post ) { + return $alternate_post->ID; + }; + add_filter( 'edac_filter_admin_post_id', $filter_callback ); + + $this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts(); + + remove_filter( 'edac_filter_admin_post_id', $filter_callback ); + + $localized_data = $wp_scripts->get_data( 'edac', 'data' ); + $this->assertStringContainsString( (string) $alternate_post->ID, $localized_data ); + } + + /** + * When the filter flags a latest-posts homepage (show_on_front=posts), the scan URL + * uses get_home_url() rather than an invalid preview link. + * + * @return void + */ + public function testScanUrlUsesHomeUrlWhenLatestPostsHomeFilterReturnsTrue() { + global $post, $pagenow, $wp_scripts; + + $post = $this->factory()->post->create_and_get( [ 'post_type' => 'page' ] ); + $pagenow = 'post.php'; + + update_option( 'show_on_front', 'posts' ); + update_option( 'page_for_posts', 0 ); + + $filter_callback = static function () { + return true; + }; + add_filter( 'edac_filter_post_is_latest_posts_home', $filter_callback ); + + $this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts(); + + remove_filter( 'edac_filter_post_is_latest_posts_home', $filter_callback ); + delete_option( 'show_on_front' ); + delete_option( 'page_for_posts' ); + + $localized_data = $wp_scripts->get_data( 'edac-editor-app', 'data' ); + $this->assertStringContainsString( 'edac_pageScanner', $localized_data ); + $this->assertStringNotContainsString( 'preview=true', $localized_data ); + // The scan URL should be based on the home URL, not a preview link. + // In WP 6.9 forward slashes are no longer escaped in json_encode output; handle both forms. + // See: https://github.com/WordPress/wordpress-develop/pull/9557. + $expected_home = esc_url_raw( trailingslashit( get_home_url() ) ); + if ( version_compare( get_bloginfo( 'version' ), '6.9', '<' ) ) { + $expected_home = str_replace( '/', '\\/', $expected_home ); + } + $this->assertStringContainsString( $expected_home, $localized_data ); + } + + /** + * Fallback case: show_on_front=page with no static front page configured also + * counts as a latest-posts homepage, so the scan URL still uses get_home_url(). + * + * @return void + */ + public function testScanUrlUsesHomeUrlWhenShowOnFrontIsPageWithNoFrontPageConfigured() { + global $post, $pagenow, $wp_scripts; + + $post = $this->factory()->post->create_and_get( [ 'post_type' => 'page' ] ); + $pagenow = 'post.php'; + + update_option( 'show_on_front', 'page' ); + delete_option( 'page_on_front' ); // No static front page configured — WP falls back to latest posts. + + $filter_callback = static function () { + return true; + }; + add_filter( 'edac_filter_post_is_latest_posts_home', $filter_callback ); + + $this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts(); + + remove_filter( 'edac_filter_post_is_latest_posts_home', $filter_callback ); + delete_option( 'show_on_front' ); + + $localized_data = $wp_scripts->get_data( 'edac-editor-app', 'data' ); + $this->assertStringContainsString( 'edac_pageScanner', $localized_data ); + $this->assertStringNotContainsString( 'preview=true', $localized_data ); + // The scan URL should be based on the home URL, not a preview link. + // In WP 6.9 forward slashes are no longer escaped in json_encode output; handle both forms. + // See: https://github.com/WordPress/wordpress-develop/pull/9557. + $expected_home = esc_url_raw( trailingslashit( get_home_url() ) ); + if ( version_compare( get_bloginfo( 'version' ), '6.9', '<' ) ) { + $expected_home = str_replace( '/', '\\/', $expected_home ); + } + $this->assertStringContainsString( $expected_home, $localized_data ); + } + + /** + * The 'active' flag must reflect the filtered post ID's type: when the filter returns a + * non-scannable post type, $active is false so the scanner doesn't run. + * + * @return void + */ + public function testActiveReflectsFilteredPostIdPostType() { + global $post, $pagenow, $wp_scripts; + + // Global $post is a 'post' type (scannable under current option). + $scannable_post = $this->factory()->post->create_and_get( [ 'post_type' => 'post' ] ); + // The filter will return a 'page' ID; make 'page' non-scannable for this test. + $non_scannable_post = $this->factory()->post->create_and_get( [ 'post_type' => 'page' ] ); + $post = $scannable_post; + $pagenow = 'post.php'; + + // Restrict scannable types to 'post' only so 'page' becomes non-scannable. + update_option( 'edac_post_types', [ 'post' ] ); + + $filter_callback = static function () use ( $non_scannable_post ) { + return $non_scannable_post->ID; + }; + add_filter( 'edac_filter_admin_post_id', $filter_callback ); + + $this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts(); + + remove_filter( 'edac_filter_admin_post_id', $filter_callback ); + // Restore original scannable post types. + update_option( 'edac_post_types', [ 'post', 'page' ] ); + + $localized_data = $wp_scripts->get_data( 'edac-editor-app', 'data' ); + $this->assertNotEmpty( $localized_data ); + // $active must be false because the filtered post type ('page') is not scannable. + // wp_localize_script serializes PHP false as "" (empty string) in older WP versions + // and may serialize it as JSON false in newer ones — accept both forms. + $this->assertMatchesRegularExpression( '/"active"\s*:\s*(false|"")/', $localized_data ); + } + /** * Helper to set a mock current screen with block editor context. * diff --git a/tests/phpunit/includes/classes/EnqueueFrontendTest.php b/tests/phpunit/includes/classes/EnqueueFrontendTest.php index 0e73e0857..3df6c0029 100644 --- a/tests/phpunit/includes/classes/EnqueueFrontendTest.php +++ b/tests/phpunit/includes/classes/EnqueueFrontendTest.php @@ -73,6 +73,58 @@ public function testScannerBundleUrlIncludesVersionQueryString(): void { $this->assertStringContainsString( 'ver=' . EDAC_VERSION, $localized_data ); } + /** + * Highlighter must NOT load on the "latest posts" homepage when no filter overrides the ID. + * + * When show_on_front=posts, the global $post is the first blog post from the main query, not + * the homepage itself. The fix passes null to the filter so the free plugin bails gracefully. + */ + public function testFrontendHighlighterDoesNotLoadOnLatestPostsHomepage(): void { + $admin_id = $this->factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin_id ); + + // Create a post so the main query has results. + $this->factory()->post->create_and_get( [ 'post_type' => 'post' ] ); + + update_option( 'show_on_front', 'posts' ); + + // Simulate visiting the homepage so is_home() / is_front_page() return true. + $this->go_to( '/' ); + + Enqueue_Frontend::maybe_enqueue_frontend_highlighter(); + + $this->assertFalse( wp_script_is( 'edac-frontend-highlighter-app', 'enqueued' ) ); + + delete_option( 'show_on_front' ); + } + + /** + * A filter on edac_filter_frontend_highlight_post_id can enable the highlighter on the + * "latest posts" homepage by supplying a valid post ID (e.g. a Pro virtual-page ID). + */ + public function testFrontendHighlighterLoadsOnLatestPostsHomepageWhenFilterProvidesId(): void { + $admin_id = $this->factory()->user->create( [ 'role' => 'administrator' ] ); + wp_set_current_user( $admin_id ); + + $post = $this->factory()->post->create_and_get( [ 'post_type' => 'post' ] ); + + update_option( 'show_on_front', 'posts' ); + + $this->go_to( '/' ); + + $filter_callback = static function () use ( $post ) { + return $post->ID; + }; + $this->added_filters['edac_filter_frontend_highlight_post_id'] = $filter_callback; + add_filter( 'edac_filter_frontend_highlight_post_id', $filter_callback ); + + Enqueue_Frontend::maybe_enqueue_frontend_highlighter(); + + $this->assertTrue( wp_script_is( 'edac-frontend-highlighter-app', 'enqueued' ) ); + + delete_option( 'show_on_front' ); + } + /** * Helper: enqueue the frontend highlighter as an admin and return the localized data string. *