From feb9ddfeefa82d29a60ecde3e8bdfa0ac87b2a02 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 9 Jan 2025 10:11:01 -0800 Subject: [PATCH 1/7] Exclude any URLs with query parameters from speculative loading if pretty permalinks are enabled. --- plugins/speculation-rules/helper.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/speculation-rules/helper.php b/plugins/speculation-rules/helper.php index 298599fb63..4c4e5e292c 100644 --- a/plugins/speculation-rules/helper.php +++ b/plugins/speculation-rules/helper.php @@ -31,7 +31,6 @@ function plsr_get_speculation_rules(): array { $base_href_exclude_paths = array( $prefixer->prefix_path_pattern( '/wp-login.php', 'site' ), $prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ), - $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ), $prefixer->prefix_path_pattern( '/*', 'uploads' ), $prefixer->prefix_path_pattern( '/*', 'content' ), $prefixer->prefix_path_pattern( '/*', 'plugins' ), @@ -39,6 +38,16 @@ function plsr_get_speculation_rules(): array { $prefixer->prefix_path_pattern( '/*', 'stylesheet' ), ); + /* + * If pretty permalinks are enabled, exclude any URLs with query parameters. + * Otherwise, exclude specifically the URLs with a `_wpnonce` query parameter. + */ + if ( (bool) get_option( 'permalink_structure' ) ) { + $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?(.+)', 'home' ); + } else { + $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ); + } + /** * Filters the paths for which speculative prerendering should be disabled. * From d7113e7ef6dfc4e080965e4f9cc2316e127cfd4a Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 9 Jan 2025 12:59:39 -0800 Subject: [PATCH 2/7] Disable speculative loading for sites without pretty permalinks by default. --- plugins/speculation-rules/hooks.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/plugins/speculation-rules/hooks.php b/plugins/speculation-rules/hooks.php index fa478b9843..1babdfc78d 100644 --- a/plugins/speculation-rules/hooks.php +++ b/plugins/speculation-rules/hooks.php @@ -19,6 +19,29 @@ * @since 1.0.0 */ function plsr_print_speculation_rules(): void { + // Skip speculative loading for sites without pretty permalinks, unless explicitly enabled. + if ( ! (bool) get_option( 'permalink_structure' ) ) { + /** + * Filters whether speculative loading should be enabled even though the site does not use pretty permalinks. + * + * Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any + * such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are + * impossible to recognize. Therefore speculative loading is disabled by default for those sites. + * + * For site owners of sites without pretty permalinks that are certain their site is not using such a pattern, + * this filter can be used to still enable speculative loading at their own risk. + * + * @since n.e.x.t + * + * @param bool $enabled Whether speculative loading is enabled even without pretty permalinks. + */ + $enabled = (bool) apply_filters( 'plsr_enabled_without_pretty_permalinks', false ); + + if ( ! $enabled ) { + return; + } + } + wp_print_inline_script_tag( (string) wp_json_encode( plsr_get_speculation_rules() ), array( 'type' => 'speculationrules' ) From cf8eeb226ee9cb86d17c048e07734125207e82f2 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 9 Jan 2025 13:00:01 -0800 Subject: [PATCH 3/7] Disable speculative loading for logged-in users. --- plugins/speculation-rules/hooks.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/speculation-rules/hooks.php b/plugins/speculation-rules/hooks.php index 1babdfc78d..86c7fed304 100644 --- a/plugins/speculation-rules/hooks.php +++ b/plugins/speculation-rules/hooks.php @@ -19,6 +19,11 @@ * @since 1.0.0 */ function plsr_print_speculation_rules(): void { + // Skip speculative loading for logged-in users. + if ( is_user_logged_in() ) { + return; + } + // Skip speculative loading for sites without pretty permalinks, unless explicitly enabled. if ( ! (bool) get_option( 'permalink_structure' ) ) { /** From face71cae73b624636e7cef55448aeeefc2506cc Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 9 Jan 2025 13:17:10 -0800 Subject: [PATCH 4/7] Fix plsr_get_speculation_rules() tests after exclusion updates. --- .../tests/test-speculation-rules-helper.php | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/plugins/speculation-rules/tests/test-speculation-rules-helper.php b/plugins/speculation-rules/tests/test-speculation-rules-helper.php index 1bdd73103f..781afa329c 100644 --- a/plugins/speculation-rules/tests/test-speculation-rules-helper.php +++ b/plugins/speculation-rules/tests/test-speculation-rules-helper.php @@ -52,14 +52,14 @@ public function test_plsr_get_speculation_rules_href_exclude_paths(): void { $this->assertSameSets( array( - 0 => '/wp-login.php', - 1 => '/wp-admin/*', - 2 => '/*\\?*(^|&)_wpnonce=*', - 3 => '/wp-content/uploads/*', - 4 => '/wp-content/*', - 5 => '/wp-content/plugins/*', - 6 => '/wp-content/themes/stylesheet/*', - 7 => '/wp-content/themes/template/*', + '/wp-login.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?*(^|&)_wpnonce=*', ), $href_exclude_paths, 'Snapshot: ' . var_export( $href_exclude_paths, true ) @@ -79,15 +79,15 @@ static function () { // Ensure the base exclude paths are still present and that the custom path was formatted correctly. $this->assertSameSets( array( - 0 => '/wp-login.php', - 1 => '/wp-admin/*', - 2 => '/*\\?*(^|&)_wpnonce=*', - 3 => '/wp-content/uploads/*', - 4 => '/wp-content/*', - 5 => '/wp-content/plugins/*', - 6 => '/wp-content/themes/stylesheet/*', - 7 => '/wp-content/themes/template/*', - 8 => '/custom-file.php', + '/wp-login.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?*(^|&)_wpnonce=*', + '/custom-file.php', ), $href_exclude_paths, 'Snapshot: ' . var_export( $href_exclude_paths, true ) @@ -118,15 +118,15 @@ static function ( $exclude_paths, $mode ) { // Also ensure keys are sequential starting from 0 (that is, that array_is_list()). $this->assertSame( array( - 0 => '/wp-login.php', - 1 => '/wp-admin/*', - 2 => '/*\\?*(^|&)_wpnonce=*', - 3 => '/wp-content/uploads/*', - 4 => '/wp-content/*', - 5 => '/wp-content/plugins/*', - 6 => '/wp-content/themes/stylesheet/*', - 7 => '/wp-content/themes/template/*', - 8 => '/products/*', + '/wp-login.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?*(^|&)_wpnonce=*', + '/products/*', ), $href_exclude_paths, 'Snapshot: ' . var_export( $href_exclude_paths, true ) @@ -141,14 +141,14 @@ static function ( $exclude_paths, $mode ) { // Ensure the additional exclusion is not present because the mode is 'prefetch'. $this->assertSame( array( - 0 => '/wp-login.php', - 1 => '/wp-admin/*', - 2 => '/*\\?*(^|&)_wpnonce=*', - 3 => '/wp-content/uploads/*', - 4 => '/wp-content/*', - 5 => '/wp-content/plugins/*', - 6 => '/wp-content/themes/stylesheet/*', - 7 => '/wp-content/themes/template/*', + '/wp-login.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?*(^|&)_wpnonce=*', ), $href_exclude_paths, 'Snapshot: ' . var_export( $href_exclude_paths, true ) @@ -177,19 +177,19 @@ static function ( array $exclude_paths ): array { $actual = plsr_get_speculation_rules()['prerender'][0]['where']['and'][1]['not']['href_matches']; $this->assertSame( array( - 0 => '/wp-login.php', - 1 => '/wp-admin/*', - 2 => '/*\\?*(^|&)_wpnonce=*', - 3 => '/wp-content/uploads/*', - 4 => '/wp-content/*', - 5 => '/wp-content/plugins/*', - 6 => '/wp-content/themes/stylesheet/*', - 7 => '/wp-content/themes/template/*', - 8 => '/unshifted/', - 9 => '/next/', - 10 => '/negative-one/', - 11 => '/one-hundred/', - 12 => '/letter-a/', + '/wp-login.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?*(^|&)_wpnonce=*', + '/unshifted/', + '/next/', + '/negative-one/', + '/one-hundred/', + '/letter-a/', ), $actual, 'Snapshot: ' . var_export( $actual, true ) @@ -225,15 +225,15 @@ static function ( array $exclude_paths ): array { $actual = plsr_get_speculation_rules()['prerender'][0]['where']['and'][1]['not']['href_matches']; $this->assertSame( array( - 0 => '/wp/wp-login.php', - 1 => '/wp/wp-admin/*', - 2 => '/blog/*\\?*(^|&)_wpnonce=*', - 3 => '/wp-content/uploads/*', - 4 => '/wp-content/*', - 5 => '/wp-content/plugins/*', - 6 => '/wp-content/themes/stylesheet/*', - 7 => '/wp-content/themes/template/*', - 8 => '/blog/store/*', + '/wp/wp-login.php', + '/wp/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/blog/*\\?*(^|&)_wpnonce=*', + '/blog/store/*', ), $actual, 'Snapshot: ' . var_export( $actual, true ) From 30274eaa846cd7606c0643980227da7f405ac423 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 9 Jan 2025 13:57:37 -0800 Subject: [PATCH 5/7] Add test coverage for new enhancements. --- .../tests/test-speculation-rules-helper.php | 25 +++++++++++ .../tests/test-speculation-rules.php | 42 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/plugins/speculation-rules/tests/test-speculation-rules-helper.php b/plugins/speculation-rules/tests/test-speculation-rules-helper.php index 781afa329c..224e647908 100644 --- a/plugins/speculation-rules/tests/test-speculation-rules-helper.php +++ b/plugins/speculation-rules/tests/test-speculation-rules-helper.php @@ -94,6 +94,31 @@ static function () { ); } + /** + * @covers ::plsr_get_speculation_rules + */ + public function test_plsr_get_speculation_rules_href_exclude_paths_with_pretty_permalinks(): void { + update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); + + $rules = plsr_get_speculation_rules(); + $href_exclude_paths = $rules['prerender'][0]['where']['and'][1]['not']['href_matches']; + + $this->assertSameSets( + array( + '/wp-login.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?(.+)', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + } + /** * @covers ::plsr_get_speculation_rules */ diff --git a/plugins/speculation-rules/tests/test-speculation-rules.php b/plugins/speculation-rules/tests/test-speculation-rules.php index caf2d9cb58..d9a3d38382 100644 --- a/plugins/speculation-rules/tests/test-speculation-rules.php +++ b/plugins/speculation-rules/tests/test-speculation-rules.php @@ -29,6 +29,8 @@ public function test_hooks(): void { * @covers ::plsr_print_speculation_rules */ public function test_plsr_print_speculation_rules_without_html5_support(): void { + $this->enable_pretty_permalinks(); + $output = get_echo( 'plsr_print_speculation_rules' ); $this->assertStringContainsString( '