Plugin Directory

Changeset 3229867


Ignore:
Timestamp:
01/27/2025 06:22:54 PM (12 months ago)
Author:
performanceteam
Message:

Update to version 1.4.0 from GitHub

Location:
speculation-rules
Files:
12 edited
1 copied

Legend:

Unmodified
Added
Removed
  • speculation-rules/tags/1.4.0/class-plsr-url-pattern-prefixer.php

    r3089572 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
     11    exit; // Exit if accessed directly.
    1212}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    3637     */
    3738    public function __construct( array $contexts = array() ) {
    38         if ( $contexts ) {
     39        if ( count( $contexts ) > 0 ) {
    3940            $this->contexts = array_map(
    4041                static function ( string $str ): string {
  • speculation-rules/tags/1.4.0/helper.php

    r3098880 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
     11    exit; // Exit if accessed directly.
    1212}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    2021 * @since 1.0.0
    2122 *
    22  * @return array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.
     23 * @return non-empty-array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.
    2324 */
    2425function plsr_get_speculation_rules(): array {
    25     $option = get_option( 'plsr_speculation_rules' );
    26 
    27     /*
    28      * This logic is only relevant for edge-cases where the setting may not be registered,
    29      * a.k.a. defensive coding.
    30      */
    31     if ( ! $option || ! is_array( $option ) ) {
    32         $option = plsr_get_setting_default();
    33     } else {
    34         $option = array_merge( plsr_get_setting_default(), $option );
    35     }
    36 
    37     $mode      = (string) $option['mode'];
     26    $option    = plsr_get_stored_setting_value();
     27    $mode      = $option['mode'];
    3828    $eagerness = $option['eagerness'];
    3929
     
    4131
    4232    $base_href_exclude_paths = array(
    43         $prefixer->prefix_path_pattern( '/wp-login.php', 'site' ),
     33        $prefixer->prefix_path_pattern( '/wp-*.php', 'site' ),
    4434        $prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ),
    45         $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ),
    4635        $prefixer->prefix_path_pattern( '/*', 'uploads' ),
    4736        $prefixer->prefix_path_pattern( '/*', 'content' ),
     
    5140    );
    5241
     42    /*
     43     * If pretty permalinks are enabled, exclude any URLs with query parameters.
     44     * Otherwise, exclude specifically the URLs with a `_wpnonce` query parameter.
     45     */
     46    if ( (bool) get_option( 'permalink_structure' ) ) {
     47        $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?(.+)', 'home' );
     48    } else {
     49        $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' );
     50    }
     51
    5352    /**
    5453     * Filters the paths for which speculative prerendering should be disabled.
    5554     *
    5655     * All paths should start in a forward slash, relative to the root document. The `*` can be used as a wildcard.
    57      * By default, the array includes `/wp-login.php` and `/wp-admin/*`.
    5856     *
    5957     * If the WordPress site is in a subdirectory, the exclude paths will automatically be prefixed as necessary.
  • speculation-rules/tags/1.4.0/hooks.php

    r3089572 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
     11    exit; // Exit if accessed directly.
    1212}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    2021 */
    2122function plsr_print_speculation_rules(): void {
    22     $rules = plsr_get_speculation_rules();
    23     if ( empty( $rules ) ) {
     23    // Skip speculative loading for logged-in users.
     24    if ( is_user_logged_in() ) {
    2425        return;
    2526    }
    2627
    27     // This workaround is needed for WP 6.4. See <https://core.trac.wordpress.org/ticket/60320>.
    28     $needs_html5_workaround = (
    29         ! current_theme_supports( 'html5', 'script' ) &&
    30         version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.4', '>=' ) &&
    31         version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.5', '<' )
    32     );
    33     if ( $needs_html5_workaround ) {
    34         $backup_wp_theme_features = $GLOBALS['_wp_theme_features'];
    35         add_theme_support( 'html5', array( 'script' ) );
     28    // Skip speculative loading for sites without pretty permalinks, unless explicitly enabled.
     29    if ( ! (bool) get_option( 'permalink_structure' ) ) {
     30        /**
     31         * Filters whether speculative loading should be enabled even though the site does not use pretty permalinks.
     32         *
     33         * Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any
     34         * such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are
     35         * impossible to recognize. Therefore speculative loading is disabled by default for those sites.
     36         *
     37         * For site owners of sites without pretty permalinks that are certain their site is not using such a pattern,
     38         * this filter can be used to still enable speculative loading at their own risk.
     39         *
     40         * @since 1.4.0
     41         *
     42         * @param bool $enabled Whether speculative loading is enabled even without pretty permalinks.
     43         */
     44        $enabled = (bool) apply_filters( 'plsr_enabled_without_pretty_permalinks', false );
     45
     46        if ( ! $enabled ) {
     47            return;
     48        }
    3649    }
    3750
    3851    wp_print_inline_script_tag(
    39         (string) wp_json_encode( $rules ),
     52        (string) wp_json_encode( plsr_get_speculation_rules() ),
    4053        array( 'type' => 'speculationrules' )
    4154    );
    42 
    43     if ( $needs_html5_workaround ) {
    44         $GLOBALS['_wp_theme_features'] = $backup_wp_theme_features; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
    45     }
    4655}
    4756add_action( 'wp_footer', 'plsr_print_speculation_rules' );
  • speculation-rules/tags/1.4.0/load.php

    r3098880 r3229867  
    33 * Plugin Name: Speculative Loading
    44 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/speculation-rules
    5  * Description: Enables browsers to speculatively prerender or prefetch pages when hovering over links.
    6  * Requires at least: 6.4
     5 * Description: Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction.
     6 * Requires at least: 6.6
    77 * Requires PHP: 7.2
    8  * Version: 1.3.1
     8 * Version: 1.4.0
    99 * Author: WordPress Performance Team
    1010 * Author URI: https://make.wordpress.org/performance/
     
    1616 */
    1717
    18 // Exit if accessed directly.
     18// @codeCoverageIgnoreStart
    1919if ( ! defined( 'ABSPATH' ) ) {
    20     exit;
     20    exit; // Exit if accessed directly.
    2121}
     22// @codeCoverageIgnoreEnd
    2223
    2324(
     
    6667)(
    6768    'plsr_pending_plugin_info',
    68     '1.3.1',
     69    '1.4.0',
    6970    static function ( string $version ): void {
    7071
  • speculation-rules/tags/1.4.0/readme.txt

    r3098880 r3229867  
    11=== Speculative Loading ===
    22
    3 Contributors:      wordpressdotorg
    4 Requires at least: 6.4
    5 Tested up to:      6.5
    6 Requires PHP:      7.2
    7 Stable tag:        1.3.1
    8 License:           GPLv2 or later
    9 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    10 Tags:              performance, javascript, speculation rules, prerender, prefetch
     3Contributors: wordpressdotorg
     4Tested up to: 6.7
     5Stable tag:   1.4.0
     6License:      GPLv2 or later
     7License URI:  https://www.gnu.org/licenses/gpl-2.0.html
     8Tags:         performance, javascript, speculation rules, prerender, prefetch
    119
    12 Enables browsers to speculatively prerender or prefetch pages when hovering over links.
     10Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction.
    1311
    1412== Description ==
    1513
    16 This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered based on user interaction.
     14This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered.
    1715
    1816See the [Speculation Rules WICG specification draft](https://wicg.github.io/nav-speculation/speculation-rules.html).
    1917
    20 By default, the plugin is configured to prerender WordPress frontend URLs when the user hovers over a relevant link. This can be customized via the "Speculative Loading" section under _Settings > Reading_.
     18By default, the plugin is configured to prerender WordPress frontend URLs when the user interacts with a relevant link. This can be customized via the "Speculative Loading" section in the _Settings > Reading_ admin screen.
    2119
    22 A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the 'no-prerender' CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.
     20A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the `no-prerender` CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.
    2321
    2422= Browser support =
    2523
    26 The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects but will not benefit from the speculative loading. Note that extensions may disable preloading by default (for example, uBlock Origin does this).
    27 
    28 Other browsers will not see any adverse effects, however the feature will not work for those clients.
     24The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects; they will simply not benefit from the speculative loading. Note that certain browser extensions may disable preloading by default.
    2925
    3026* [Browser support for the Speculation Rules API in general](https://caniuse.com/mdn-html_elements_script_type_speculationrules)
    31 * [Information on document rules syntax support used by the plugin](https://developer.chrome.com/blog/chrome-121-beta#speculation_rules_api)
     27* [Information on document rules syntax support used by the plugin](https://developer.chrome.com/docs/web-platform/prerender-pages)
    3228
    3329_This plugin was formerly known as Speculation Rules._
     
    5147= How can I prevent certain URLs from being prefetched and prerendered? =
    5248
    53 Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URL generated with `wp_nonce_url()` (or which contain the `_wpnonce` query var) is also ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter.
     49Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URLs generated with `wp_nonce_url()` (or which contains the `_wpnonce` query var) and `nofollow` links are also ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter.
    5450
    55 This example would ensure that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` would be excluded from prefetching and prerendering.
     51The following example ensures that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` are excluded from prefetching and prerendering:
    5652`
    5753<?php
    58 
    5954add_filter(
    6055    'plsr_speculation_rules_href_exclude_paths',
     
    7065For this purpose, the `plsr_speculation_rules_href_exclude_paths` filter receives the current mode (either "prefetch" or "prerender") to provide conditional exclusions.
    7166
    72 The following example would ensure that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched.
     67The following example ensures that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched:
    7368`
    7469<?php
    75 
    7670add_filter(
    7771    'plsr_speculation_rules_href_exclude_paths',
     
    8983As mentioned above, adding the `no-prerender` CSS class to a link will prevent it from being prerendered (but not prefetched). Additionally, links with `rel=nofollow` will neither be prefetched nor prerendered because some plugins add this to non-idempotent links (e.g. add to cart); such links ideally should rather be buttons which trigger a POST request or at least they should use `wp_nonce_url()`.
    9084
     85= Are there any special considerations for speculative loading behavior? =
     86
     87For safety reasons, the entire speculative loading feature is disabled by default for logged-in users and for sites that do not use pretty permalinks. The latter is the case because plugins often use URLs with custom query parameters to let users perform actions, and such URLs should not be speculatively loaded. For sites without pretty permalinks, it is impossible or at least extremely complex to differentiate between which query parameters are Core defaults and which query parameters are custom.
     88
     89If you are running this plugin on a site without pretty permalinks and are confident that there are no custom query parameters in use that can cause state changes, you can opt in to enabling speculative loading via the `plsr_enabled_without_pretty_permalinks` filter:
     90
     91`
     92<?php
     93add_filter( 'plsr_enabled_without_pretty_permalinks', '__return_true' );
     94`
     95
    9196= How will this impact analytics and personalization? =
    9297
    9398Prerendering can affect analytics and personalization.
    9499
    95 For client-side JavaScript, is recommended to delay these until the page clicks and some solutions (like Google Analytics) already do this automatically for prerender. See [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here.
     100For client-side JavaScript, is recommended to delay these until the prerender is activated (for example by clicking on the link). Some solutions (like Google Analytics) already do this automatically, see [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here.
    96101
    97 Speculating on hover (moderate) increases the chance the page will be loaded, over preloading without this signal, and thus reduces the risk here. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which further reduces the risk here and is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain.
     102Speculating with the default `moderate` eagerness decreases the risk that the prerendered page will not be visited by the user and therefore will avoid any side effects of loading such a link in advance. In contrast, `eager` speculation increases the risk that prerendered pages may not be loaded. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which reduces the risk even further and is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain.
    98103
    99 A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) may require a new page load, and hence a new prerender anyway, which will take these into account. But it definitely is something to be aware of and test!
     104A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) often require a new page load, and hence a new prerender, which will then take these into account. But it definitely is something to be aware of and test! Prerendered pages can be canceled by removing the speculation rules `<script>` element from the page using standard JavaScript DOM APIs should this be needed when state changes without a new page load.
    100105
    101106= Where can I submit my plugin feedback? =
     
    114119
    115120== Changelog ==
     121
     122= 1.4.0 =
     123
     124**Enhancements**
     125
     126* Implement speculative loading considerations for safer behavior. ([1784](https://github.com/WordPress/performance/pull/1784))
    116127
    117128= 1.3.1 =
  • speculation-rules/tags/1.4.0/settings.php

    r3089572 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
    12 }
     11    exit; // Exit if accessed directly.
     12}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    1718 * @since 1.0.0
    1819 *
    19  * @return array<string, string> Associative array of `$mode => $label` pairs.
     20 * @return array{ prefetch: string, prerender: string } Associative array of `$mode => $label` pairs.
    2021 */
    2122function plsr_get_mode_labels(): array {
     
    3132 * @since 1.0.0
    3233 *
    33  * @return array<string, string> Associative array of `$eagerness => $label` pairs.
     34 * @return array{ conservative: string, moderate: string, eager: string } Associative array of `$eagerness => $label` pairs.
    3435 */
    3536function plsr_get_eagerness_labels(): array {
     
    4647 * @since 1.0.0
    4748 *
    48  * @return array<string, string> {
     49 * @return array{ mode: 'prerender', eagerness: 'moderate' } {
    4950 *     Default setting value.
    5051 *
     
    6162
    6263/**
    63  * Sanitizes the setting for Speculative Loading configuration.
    64  *
    65  * @since 1.0.0
    66  *
    67  * @param mixed $input Setting to sanitize.
    68  * @return array<string, string> {
    69  *     Sanitized setting.
     64 * Returns the stored setting value for Speculative Loading configuration.
     65 *
     66 * @since 1.4.0
     67 *
     68 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } {
     69 *     Stored setting value.
    7070 *
    7171 *     @type string $mode      Mode.
     
    7373 * }
    7474 */
     75function plsr_get_stored_setting_value(): array {
     76    return plsr_sanitize_setting( get_option( 'plsr_speculation_rules' ) );
     77}
     78
     79/**
     80 * Sanitizes the setting for Speculative Loading configuration.
     81 *
     82 * @since 1.0.0
     83 * @todo  Consider whether the JSON schema for the setting could be reused here.
     84 *
     85 * @param mixed $input Setting to sanitize.
     86 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } {
     87 *     Sanitized setting.
     88 *
     89 *     @type string $mode      Mode.
     90 *     @type string $eagerness Eagerness.
     91 * }
     92 */
    7593function plsr_sanitize_setting( $input ): array {
    7694    $default_value = plsr_get_setting_default();
     
    8098    }
    8199
    82     $mode_labels      = plsr_get_mode_labels();
    83     $eagerness_labels = plsr_get_eagerness_labels();
    84 
    85100    // Ensure only valid keys are present.
    86     $value = array_intersect_key( $input, $default_value );
    87 
    88     // Set any missing or invalid values to their defaults.
    89     if ( ! isset( $value['mode'] ) || ! isset( $mode_labels[ $value['mode'] ] ) ) {
     101    $value = array_intersect_key( array_merge( $default_value, $input ), $default_value );
     102
     103    // Constrain values to what is allowed.
     104    if ( ! in_array( $value['mode'], array_keys( plsr_get_mode_labels() ), true ) ) {
    90105        $value['mode'] = $default_value['mode'];
    91106    }
    92     if ( ! isset( $value['eagerness'] ) || ! isset( $eagerness_labels[ $value['eagerness'] ] ) ) {
     107    if ( ! in_array( $value['eagerness'], array_keys( plsr_get_eagerness_labels() ), true ) ) {
    93108        $value['eagerness'] = $default_value['eagerness'];
    94109    }
     
    114129            'show_in_rest'      => array(
    115130                'schema' => array(
    116                     'properties' => array(
     131                    'type'                 => 'object',
     132                    'properties'           => array(
    117133                        'mode'      => array(
    118134                            'description' => __( 'Whether to prefetch or prerender URLs.', 'speculation-rules' ),
     
    126142                        ),
    127143                    ),
     144                    'additionalProperties' => false,
    128145                ),
    129146            ),
     
    189206 * @access private
    190207 *
    191  * @param array<string, string> $args {
     208 * @param array{ field: 'mode'|'eagerness', title: non-empty-string, description: non-empty-string } $args {
    192209 *     Associative array of arguments.
    193210 *
     
    198215 */
    199216function plsr_render_settings_field( array $args ): void {
    200     if ( empty( $args['field'] ) || empty( $args['title'] ) ) { // Invalid.
    201         return;
    202     }
    203 
    204     $option = get_option( 'plsr_speculation_rules' );
    205     if ( ! isset( $option[ $args['field'] ] ) ) { // Invalid.
    206         return;
    207     }
    208 
    209     $value    = $option[ $args['field'] ];
    210     $callback = "plsr_get_{$args['field']}_labels";
    211     if ( ! is_callable( $callback ) ) {
    212         return;
    213     }
    214     $choices = call_user_func( $callback );
    215 
     217    $option = plsr_get_stored_setting_value();
     218
     219    switch ( $args['field'] ) {
     220        case 'mode':
     221            $choices = plsr_get_mode_labels();
     222            break;
     223        case 'eagerness':
     224            $choices = plsr_get_eagerness_labels();
     225            break;
     226        default:
     227            return; // Invalid (and this case should never occur).
     228    }
     229
     230    $value = $option[ $args['field'] ];
    216231    ?>
    217232    <fieldset>
    218233        <legend class="screen-reader-text"><?php echo esc_html( $args['title'] ); ?></legend>
    219         <?php
    220         foreach ( $choices as $slug => $label ) {
    221             ?>
     234        <?php foreach ( $choices as $slug => $label ) : ?>
    222235            <p>
    223236                <label>
     
    231244                </label>
    232245            </p>
    233             <?php
    234         }
    235 
    236         if ( ! empty( $args['description'] ) ) {
    237             ?>
    238             <p class="description" style="max-width: 800px;">
    239                 <?php echo esc_html( $args['description'] ); ?>
    240             </p>
    241             <?php
    242         }
    243         ?>
     246        <?php endforeach; ?>
     247
     248        <p class="description" style="max-width: 800px;">
     249            <?php echo esc_html( $args['description'] ); ?>
     250        </p>
    244251    </fieldset>
    245252    <?php
  • speculation-rules/trunk/class-plsr-url-pattern-prefixer.php

    r3089572 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
     11    exit; // Exit if accessed directly.
    1212}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    3637     */
    3738    public function __construct( array $contexts = array() ) {
    38         if ( $contexts ) {
     39        if ( count( $contexts ) > 0 ) {
    3940            $this->contexts = array_map(
    4041                static function ( string $str ): string {
  • speculation-rules/trunk/helper.php

    r3098880 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
     11    exit; // Exit if accessed directly.
    1212}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    2021 * @since 1.0.0
    2122 *
    22  * @return array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.
     23 * @return non-empty-array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.
    2324 */
    2425function plsr_get_speculation_rules(): array {
    25     $option = get_option( 'plsr_speculation_rules' );
    26 
    27     /*
    28      * This logic is only relevant for edge-cases where the setting may not be registered,
    29      * a.k.a. defensive coding.
    30      */
    31     if ( ! $option || ! is_array( $option ) ) {
    32         $option = plsr_get_setting_default();
    33     } else {
    34         $option = array_merge( plsr_get_setting_default(), $option );
    35     }
    36 
    37     $mode      = (string) $option['mode'];
     26    $option    = plsr_get_stored_setting_value();
     27    $mode      = $option['mode'];
    3828    $eagerness = $option['eagerness'];
    3929
     
    4131
    4232    $base_href_exclude_paths = array(
    43         $prefixer->prefix_path_pattern( '/wp-login.php', 'site' ),
     33        $prefixer->prefix_path_pattern( '/wp-*.php', 'site' ),
    4434        $prefixer->prefix_path_pattern( '/wp-admin/*', 'site' ),
    45         $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' ),
    4635        $prefixer->prefix_path_pattern( '/*', 'uploads' ),
    4736        $prefixer->prefix_path_pattern( '/*', 'content' ),
     
    5140    );
    5241
     42    /*
     43     * If pretty permalinks are enabled, exclude any URLs with query parameters.
     44     * Otherwise, exclude specifically the URLs with a `_wpnonce` query parameter.
     45     */
     46    if ( (bool) get_option( 'permalink_structure' ) ) {
     47        $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?(.+)', 'home' );
     48    } else {
     49        $base_href_exclude_paths[] = $prefixer->prefix_path_pattern( '/*\\?*(^|&)_wpnonce=*', 'home' );
     50    }
     51
    5352    /**
    5453     * Filters the paths for which speculative prerendering should be disabled.
    5554     *
    5655     * All paths should start in a forward slash, relative to the root document. The `*` can be used as a wildcard.
    57      * By default, the array includes `/wp-login.php` and `/wp-admin/*`.
    5856     *
    5957     * If the WordPress site is in a subdirectory, the exclude paths will automatically be prefixed as necessary.
  • speculation-rules/trunk/hooks.php

    r3089572 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
     11    exit; // Exit if accessed directly.
    1212}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    2021 */
    2122function plsr_print_speculation_rules(): void {
    22     $rules = plsr_get_speculation_rules();
    23     if ( empty( $rules ) ) {
     23    // Skip speculative loading for logged-in users.
     24    if ( is_user_logged_in() ) {
    2425        return;
    2526    }
    2627
    27     // This workaround is needed for WP 6.4. See <https://core.trac.wordpress.org/ticket/60320>.
    28     $needs_html5_workaround = (
    29         ! current_theme_supports( 'html5', 'script' ) &&
    30         version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.4', '>=' ) &&
    31         version_compare( (string) strtok( (string) get_bloginfo( 'version' ), '-' ), '6.5', '<' )
    32     );
    33     if ( $needs_html5_workaround ) {
    34         $backup_wp_theme_features = $GLOBALS['_wp_theme_features'];
    35         add_theme_support( 'html5', array( 'script' ) );
     28    // Skip speculative loading for sites without pretty permalinks, unless explicitly enabled.
     29    if ( ! (bool) get_option( 'permalink_structure' ) ) {
     30        /**
     31         * Filters whether speculative loading should be enabled even though the site does not use pretty permalinks.
     32         *
     33         * Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any
     34         * such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are
     35         * impossible to recognize. Therefore speculative loading is disabled by default for those sites.
     36         *
     37         * For site owners of sites without pretty permalinks that are certain their site is not using such a pattern,
     38         * this filter can be used to still enable speculative loading at their own risk.
     39         *
     40         * @since 1.4.0
     41         *
     42         * @param bool $enabled Whether speculative loading is enabled even without pretty permalinks.
     43         */
     44        $enabled = (bool) apply_filters( 'plsr_enabled_without_pretty_permalinks', false );
     45
     46        if ( ! $enabled ) {
     47            return;
     48        }
    3649    }
    3750
    3851    wp_print_inline_script_tag(
    39         (string) wp_json_encode( $rules ),
     52        (string) wp_json_encode( plsr_get_speculation_rules() ),
    4053        array( 'type' => 'speculationrules' )
    4154    );
    42 
    43     if ( $needs_html5_workaround ) {
    44         $GLOBALS['_wp_theme_features'] = $backup_wp_theme_features; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
    45     }
    4655}
    4756add_action( 'wp_footer', 'plsr_print_speculation_rules' );
  • speculation-rules/trunk/load.php

    r3098880 r3229867  
    33 * Plugin Name: Speculative Loading
    44 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/speculation-rules
    5  * Description: Enables browsers to speculatively prerender or prefetch pages when hovering over links.
    6  * Requires at least: 6.4
     5 * Description: Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction.
     6 * Requires at least: 6.6
    77 * Requires PHP: 7.2
    8  * Version: 1.3.1
     8 * Version: 1.4.0
    99 * Author: WordPress Performance Team
    1010 * Author URI: https://make.wordpress.org/performance/
     
    1616 */
    1717
    18 // Exit if accessed directly.
     18// @codeCoverageIgnoreStart
    1919if ( ! defined( 'ABSPATH' ) ) {
    20     exit;
     20    exit; // Exit if accessed directly.
    2121}
     22// @codeCoverageIgnoreEnd
    2223
    2324(
     
    6667)(
    6768    'plsr_pending_plugin_info',
    68     '1.3.1',
     69    '1.4.0',
    6970    static function ( string $version ): void {
    7071
  • speculation-rules/trunk/readme.txt

    r3098880 r3229867  
    11=== Speculative Loading ===
    22
    3 Contributors:      wordpressdotorg
    4 Requires at least: 6.4
    5 Tested up to:      6.5
    6 Requires PHP:      7.2
    7 Stable tag:        1.3.1
    8 License:           GPLv2 or later
    9 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
    10 Tags:              performance, javascript, speculation rules, prerender, prefetch
     3Contributors: wordpressdotorg
     4Tested up to: 6.7
     5Stable tag:   1.4.0
     6License:      GPLv2 or later
     7License URI:  https://www.gnu.org/licenses/gpl-2.0.html
     8Tags:         performance, javascript, speculation rules, prerender, prefetch
    119
    12 Enables browsers to speculatively prerender or prefetch pages when hovering over links.
     10Enables browsers to speculatively prerender or prefetch pages to achieve near-instant loads based on user interaction.
    1311
    1412== Description ==
    1513
    16 This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered based on user interaction.
     14This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered.
    1715
    1816See the [Speculation Rules WICG specification draft](https://wicg.github.io/nav-speculation/speculation-rules.html).
    1917
    20 By default, the plugin is configured to prerender WordPress frontend URLs when the user hovers over a relevant link. This can be customized via the "Speculative Loading" section under _Settings > Reading_.
     18By default, the plugin is configured to prerender WordPress frontend URLs when the user interacts with a relevant link. This can be customized via the "Speculative Loading" section in the _Settings > Reading_ admin screen.
    2119
    22 A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the 'no-prerender' CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.
     20A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the `no-prerender` CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.
    2321
    2422= Browser support =
    2523
    26 The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects but will not benefit from the speculative loading. Note that extensions may disable preloading by default (for example, uBlock Origin does this).
    27 
    28 Other browsers will not see any adverse effects, however the feature will not work for those clients.
     24The Speculation Rules API is a new web API, and the functionality used by the plugin is supported in Chromium-based browsers such as Chrome, Edge, or Opera using version 121 or above. Other browsers such as Safari and Firefox will ignore the functionality with no ill effects; they will simply not benefit from the speculative loading. Note that certain browser extensions may disable preloading by default.
    2925
    3026* [Browser support for the Speculation Rules API in general](https://caniuse.com/mdn-html_elements_script_type_speculationrules)
    31 * [Information on document rules syntax support used by the plugin](https://developer.chrome.com/blog/chrome-121-beta#speculation_rules_api)
     27* [Information on document rules syntax support used by the plugin](https://developer.chrome.com/docs/web-platform/prerender-pages)
    3228
    3329_This plugin was formerly known as Speculation Rules._
     
    5147= How can I prevent certain URLs from being prefetched and prerendered? =
    5248
    53 Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URL generated with `wp_nonce_url()` (or which contain the `_wpnonce` query var) is also ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter.
     49Not every URL can be reasonably prerendered. Prerendering static content is typically reliable, however prerendering interactive content, such as a logout URL, can lead to issues. For this reason, certain WordPress core URLs such as `/wp-login.php` and `/wp-admin/*` are excluded from prefetching and prerendering. Additionally, any URLs generated with `wp_nonce_url()` (or which contains the `_wpnonce` query var) and `nofollow` links are also ignored. You can exclude additional URL patterns by using the `plsr_speculation_rules_href_exclude_paths` filter.
    5450
    55 This example would ensure that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` would be excluded from prefetching and prerendering.
     51The following example ensures that URLs like `https://example.com/cart/` or `https://example.com/cart/foo` are excluded from prefetching and prerendering:
    5652`
    5753<?php
    58 
    5954add_filter(
    6055    'plsr_speculation_rules_href_exclude_paths',
     
    7065For this purpose, the `plsr_speculation_rules_href_exclude_paths` filter receives the current mode (either "prefetch" or "prerender") to provide conditional exclusions.
    7166
    72 The following example would ensure that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched.
     67The following example ensures that URLs like `https://example.com/products/...` cannot be prerendered, while still allowing them to be prefetched:
    7368`
    7469<?php
    75 
    7670add_filter(
    7771    'plsr_speculation_rules_href_exclude_paths',
     
    8983As mentioned above, adding the `no-prerender` CSS class to a link will prevent it from being prerendered (but not prefetched). Additionally, links with `rel=nofollow` will neither be prefetched nor prerendered because some plugins add this to non-idempotent links (e.g. add to cart); such links ideally should rather be buttons which trigger a POST request or at least they should use `wp_nonce_url()`.
    9084
     85= Are there any special considerations for speculative loading behavior? =
     86
     87For safety reasons, the entire speculative loading feature is disabled by default for logged-in users and for sites that do not use pretty permalinks. The latter is the case because plugins often use URLs with custom query parameters to let users perform actions, and such URLs should not be speculatively loaded. For sites without pretty permalinks, it is impossible or at least extremely complex to differentiate between which query parameters are Core defaults and which query parameters are custom.
     88
     89If you are running this plugin on a site without pretty permalinks and are confident that there are no custom query parameters in use that can cause state changes, you can opt in to enabling speculative loading via the `plsr_enabled_without_pretty_permalinks` filter:
     90
     91`
     92<?php
     93add_filter( 'plsr_enabled_without_pretty_permalinks', '__return_true' );
     94`
     95
    9196= How will this impact analytics and personalization? =
    9297
    9398Prerendering can affect analytics and personalization.
    9499
    95 For client-side JavaScript, is recommended to delay these until the page clicks and some solutions (like Google Analytics) already do this automatically for prerender. See [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here.
     100For client-side JavaScript, is recommended to delay these until the prerender is activated (for example by clicking on the link). Some solutions (like Google Analytics) already do this automatically, see [Impact on Analytics](https://developer.chrome.com/docs/web-platform/prerender-pages#impact-on-analytics). Additionally, cross-origin iframes are not loaded until activation which can further avoid issues here.
    96101
    97 Speculating on hover (moderate) increases the chance the page will be loaded, over preloading without this signal, and thus reduces the risk here. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which further reduces the risk here and is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain.
     102Speculating with the default `moderate` eagerness decreases the risk that the prerendered page will not be visited by the user and therefore will avoid any side effects of loading such a link in advance. In contrast, `eager` speculation increases the risk that prerendered pages may not be loaded. Alternatively, the plugin offers to only speculate on mouse/pointer down (conservative) which reduces the risk even further and is an option for sites which are concerned about this, at the cost of having less of a lead time and so less of a performance gain.
    98103
    99 A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) may require a new page load, and hence a new prerender anyway, which will take these into account. But it definitely is something to be aware of and test!
     104A prerendered page is linked to the page that prerenders it, so personalisation may already be known by this point and changes (e.g. browsing other products, or logging in/out) often require a new page load, and hence a new prerender, which will then take these into account. But it definitely is something to be aware of and test! Prerendered pages can be canceled by removing the speculation rules `<script>` element from the page using standard JavaScript DOM APIs should this be needed when state changes without a new page load.
    100105
    101106= Where can I submit my plugin feedback? =
     
    114119
    115120== Changelog ==
     121
     122= 1.4.0 =
     123
     124**Enhancements**
     125
     126* Implement speculative loading considerations for safer behavior. ([1784](https://github.com/WordPress/performance/pull/1784))
    116127
    117128= 1.3.1 =
  • speculation-rules/trunk/settings.php

    r3089572 r3229867  
    77 */
    88
    9 // Exit if accessed directly.
     9// @codeCoverageIgnoreStart
    1010if ( ! defined( 'ABSPATH' ) ) {
    11     exit;
    12 }
     11    exit; // Exit if accessed directly.
     12}
     13// @codeCoverageIgnoreEnd
    1314
    1415/**
     
    1718 * @since 1.0.0
    1819 *
    19  * @return array<string, string> Associative array of `$mode => $label` pairs.
     20 * @return array{ prefetch: string, prerender: string } Associative array of `$mode => $label` pairs.
    2021 */
    2122function plsr_get_mode_labels(): array {
     
    3132 * @since 1.0.0
    3233 *
    33  * @return array<string, string> Associative array of `$eagerness => $label` pairs.
     34 * @return array{ conservative: string, moderate: string, eager: string } Associative array of `$eagerness => $label` pairs.
    3435 */
    3536function plsr_get_eagerness_labels(): array {
     
    4647 * @since 1.0.0
    4748 *
    48  * @return array<string, string> {
     49 * @return array{ mode: 'prerender', eagerness: 'moderate' } {
    4950 *     Default setting value.
    5051 *
     
    6162
    6263/**
    63  * Sanitizes the setting for Speculative Loading configuration.
    64  *
    65  * @since 1.0.0
    66  *
    67  * @param mixed $input Setting to sanitize.
    68  * @return array<string, string> {
    69  *     Sanitized setting.
     64 * Returns the stored setting value for Speculative Loading configuration.
     65 *
     66 * @since 1.4.0
     67 *
     68 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } {
     69 *     Stored setting value.
    7070 *
    7171 *     @type string $mode      Mode.
     
    7373 * }
    7474 */
     75function plsr_get_stored_setting_value(): array {
     76    return plsr_sanitize_setting( get_option( 'plsr_speculation_rules' ) );
     77}
     78
     79/**
     80 * Sanitizes the setting for Speculative Loading configuration.
     81 *
     82 * @since 1.0.0
     83 * @todo  Consider whether the JSON schema for the setting could be reused here.
     84 *
     85 * @param mixed $input Setting to sanitize.
     86 * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } {
     87 *     Sanitized setting.
     88 *
     89 *     @type string $mode      Mode.
     90 *     @type string $eagerness Eagerness.
     91 * }
     92 */
    7593function plsr_sanitize_setting( $input ): array {
    7694    $default_value = plsr_get_setting_default();
     
    8098    }
    8199
    82     $mode_labels      = plsr_get_mode_labels();
    83     $eagerness_labels = plsr_get_eagerness_labels();
    84 
    85100    // Ensure only valid keys are present.
    86     $value = array_intersect_key( $input, $default_value );
    87 
    88     // Set any missing or invalid values to their defaults.
    89     if ( ! isset( $value['mode'] ) || ! isset( $mode_labels[ $value['mode'] ] ) ) {
     101    $value = array_intersect_key( array_merge( $default_value, $input ), $default_value );
     102
     103    // Constrain values to what is allowed.
     104    if ( ! in_array( $value['mode'], array_keys( plsr_get_mode_labels() ), true ) ) {
    90105        $value['mode'] = $default_value['mode'];
    91106    }
    92     if ( ! isset( $value['eagerness'] ) || ! isset( $eagerness_labels[ $value['eagerness'] ] ) ) {
     107    if ( ! in_array( $value['eagerness'], array_keys( plsr_get_eagerness_labels() ), true ) ) {
    93108        $value['eagerness'] = $default_value['eagerness'];
    94109    }
     
    114129            'show_in_rest'      => array(
    115130                'schema' => array(
    116                     'properties' => array(
     131                    'type'                 => 'object',
     132                    'properties'           => array(
    117133                        'mode'      => array(
    118134                            'description' => __( 'Whether to prefetch or prerender URLs.', 'speculation-rules' ),
     
    126142                        ),
    127143                    ),
     144                    'additionalProperties' => false,
    128145                ),
    129146            ),
     
    189206 * @access private
    190207 *
    191  * @param array<string, string> $args {
     208 * @param array{ field: 'mode'|'eagerness', title: non-empty-string, description: non-empty-string } $args {
    192209 *     Associative array of arguments.
    193210 *
     
    198215 */
    199216function plsr_render_settings_field( array $args ): void {
    200     if ( empty( $args['field'] ) || empty( $args['title'] ) ) { // Invalid.
    201         return;
    202     }
    203 
    204     $option = get_option( 'plsr_speculation_rules' );
    205     if ( ! isset( $option[ $args['field'] ] ) ) { // Invalid.
    206         return;
    207     }
    208 
    209     $value    = $option[ $args['field'] ];
    210     $callback = "plsr_get_{$args['field']}_labels";
    211     if ( ! is_callable( $callback ) ) {
    212         return;
    213     }
    214     $choices = call_user_func( $callback );
    215 
     217    $option = plsr_get_stored_setting_value();
     218
     219    switch ( $args['field'] ) {
     220        case 'mode':
     221            $choices = plsr_get_mode_labels();
     222            break;
     223        case 'eagerness':
     224            $choices = plsr_get_eagerness_labels();
     225            break;
     226        default:
     227            return; // Invalid (and this case should never occur).
     228    }
     229
     230    $value = $option[ $args['field'] ];
    216231    ?>
    217232    <fieldset>
    218233        <legend class="screen-reader-text"><?php echo esc_html( $args['title'] ); ?></legend>
    219         <?php
    220         foreach ( $choices as $slug => $label ) {
    221             ?>
     234        <?php foreach ( $choices as $slug => $label ) : ?>
    222235            <p>
    223236                <label>
     
    231244                </label>
    232245            </p>
    233             <?php
    234         }
    235 
    236         if ( ! empty( $args['description'] ) ) {
    237             ?>
    238             <p class="description" style="max-width: 800px;">
    239                 <?php echo esc_html( $args['description'] ); ?>
    240             </p>
    241             <?php
    242         }
    243         ?>
     246        <?php endforeach; ?>
     247
     248        <p class="description" style="max-width: 800px;">
     249            <?php echo esc_html( $args['description'] ); ?>
     250        </p>
    244251    </fieldset>
    245252    <?php
Note: See TracChangeset for help on using the changeset viewer.