Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a19089
Port speculative loading implementation to core, using filter instead…
felixarntz Nov 21, 2024
061819d
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Nov 21, 2024
7448b4c
Fix WPCS violation.
felixarntz Nov 21, 2024
f65e1a5
Fix more WPCS issues.
felixarntz Nov 21, 2024
694c1b1
Add missing translator comment.
felixarntz Nov 21, 2024
f38167c
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Nov 25, 2024
cc66024
Use null instead of false to disable speculative loading.
felixarntz Nov 25, 2024
1485faf
Fix WPCS.
felixarntz Nov 25, 2024
a78f8f5
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Jan 9, 2025
535c6f8
Exclude URLs with query parameters when pretty permalinks are enabled…
felixarntz Jan 9, 2025
a8c27e8
Disable speculative loading by default for logged-in users.
felixarntz Jan 9, 2025
a0d4dda
Exclude not only wp-login.php but any wp- PHP files from the WordPres…
felixarntz Jan 9, 2025
4bb68cb
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Jan 10, 2025
70b6204
Use coversDefaultClass in tests.
felixarntz Jan 10, 2025
8a55d2f
Make data provider methods static.
felixarntz Jan 10, 2025
cae7faa
Merge branch 'add/62503-speculative-loading' of github.com:felixarntz…
felixarntz Jan 10, 2025
da5fb19
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Jan 30, 2025
04d41c7
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Feb 3, 2025
b267ae5
Allow providing additional speculation rules by amending speculation …
felixarntz Feb 3, 2025
aa50b92
Add tests for WP_Speculation_Rules class and fix bugs found by tests.
felixarntz Feb 3, 2025
8c12bd3
Add support for the eagerness value immediate.
felixarntz Feb 3, 2025
6827503
Fix prerender exclusions so that they also exclude links that opt out…
felixarntz Feb 3, 2025
4d54a08
Fix WPCS errors.
felixarntz Feb 3, 2025
82a6ef6
Make `WP_Speculation_Rules` final.
felixarntz Feb 3, 2025
d4c382d
Remove unnecessary to_array() method.
felixarntz Feb 3, 2025
b85f0b8
Make comment more future-proof.
felixarntz Feb 5, 2025
066a1f7
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Feb 5, 2025
5093344
Merge branch 'add/62503-speculative-loading' of github.com:felixarntz…
felixarntz Feb 5, 2025
b587733
Avoid using we in inline comments and use encouraged syntax for multi…
felixarntz Feb 5, 2025
a9c52f8
Disallow use of immediate eagerness for document-level rules.
felixarntz Feb 5, 2025
a704a2b
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Feb 6, 2025
b08592a
For sites without pretty permalinks, exclude URLs using any kind of n…
felixarntz Feb 6, 2025
0a798ff
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Feb 12, 2025
349f4c0
Remove unnecessary parameter from wp_get_speculation_rules() and inst…
felixarntz Feb 12, 2025
029324e
Move speculative loading validation functions to become static method…
felixarntz Feb 12, 2025
f290c26
Use static callbacks in tests.
felixarntz Feb 14, 2025
57fd8c1
Merge branch 'trunk' into add/62503-speculative-loading
felixarntz Feb 18, 2025
b330805
Merge branch 'add/62503-speculative-loading' of github.com:felixarntz…
felixarntz Feb 18, 2025
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
293 changes: 293 additions & 0 deletions src/wp-includes/class-wp-speculation-rules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
<?php
/**
* Class 'WP_Speculation_Rules'.
*
* @package WordPress
* @subpackage Speculative Loading
* @since 6.8.0
*/

/**
* Class representing a set of speculation rules.
*
* @since 6.8.0
* @access private
*/
final class WP_Speculation_Rules implements JsonSerializable {

/**
* Stored rules, as a map of `$mode => $rules` pairs.
*
* Every `$rules` value is a map of `$id => $rule` pairs.
*
* @since 6.8.0
* @var array<string, array<string, mixed>>
*/
private $rules_by_mode = array();

/**
* The allowed speculation rules modes as a map, used for validation.
*
* @since 6.8.0
* @var array<string, bool>
*/
private static $mode_allowlist = array(
'prefetch' => true,
'prerender' => true,
);

/**
* The allowed speculation rules eagerness levels as a map, used for validation.
*
* @since 6.8.0
* @var array<string, bool>
*/
private static $eagerness_allowlist = array(
'immediate' => true,
'eager' => true,
'moderate' => true,
'conservative' => true,
);

/**
* The allowed speculation rules sources as a map, used for validation.
*
* @since 6.8.0
* @var array<string, bool>
*/
private static $source_allowlist = array(
'list' => true,
'document' => true,
);

/**
* Adds a speculation rule to the speculation rules to consider.
*
* @since 6.8.0
*
* @param string $mode Speculative loading mode. Either 'prefetch' or 'prerender'.
* @param string $id Unique string identifier for the speculation rule.
* @param array<string, mixed> $rule Associative array of rule arguments.
* @return bool True on success, false if invalid parameters are provided.
*/
public function add_rule( string $mode, string $id, array $rule ): bool {
if ( ! self::is_valid_mode( $mode ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: invalid mode value */
__( 'The value "%s" is not a valid speculation rules mode.' ),
esc_html( $mode )
),
'6.8.0'
);
return false;
}

if ( ! $this->is_valid_id( $id ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: invalid ID value */
__( 'The value "%s" is not a valid ID for a speculation rule.' ),
esc_html( $id )
),
'6.8.0'
);
return false;
}

if ( $this->has_rule( $mode, $id ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: invalid ID value */
__( 'A speculation rule with ID "%s" already exists.' ),
esc_html( $id )
),
'6.8.0'
);
return false;
}

/*
* Perform some basic speculation rule validation.
* Every rule must have either a 'where' key or a 'urls' key, but not both.
* The presence of a 'where' key implies a 'source' of 'document', while the presence of a 'urls' key implies
* a 'source' of 'list'.
*/
if (
( ! isset( $rule['where'] ) && ! isset( $rule['urls'] ) ) ||
( isset( $rule['where'] ) && isset( $rule['urls'] ) )
) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: allowed key, 2: alternative allowed key */
__( 'A speculation rule must include either a "%1$s" key or a "%2$s" key, but not both.' ),
'where',
'urls'
),
'6.8.0'
);
return false;
}
if ( isset( $rule['source'] ) ) {
if ( ! self::is_valid_source( $rule['source'] ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: invalid source value */
__( 'The value "%s" is not a valid source for a speculation rule.' ),
esc_html( $rule['source'] )
),
'6.8.0'
);
return false;
}

if ( 'list' === $rule['source'] && isset( $rule['where'] ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: source value, 2: forbidden key */
__( 'A speculation rule of source "%1$s" must not include a "%2$s" key.' ),
'list',
'where'
),
'6.8.0'
);
return false;
}

if ( 'document' === $rule['source'] && isset( $rule['urls'] ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: 1: source value, 2: forbidden key */
__( 'A speculation rule of source "%1$s" must not include a "%2$s" key.' ),
'document',
'urls'
),
'6.8.0'
);
return false;
}
}

// If there is an 'eagerness' key specified, make sure it's valid.
if ( isset( $rule['eagerness'] ) ) {
if ( ! self::is_valid_eagerness( $rule['eagerness'] ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: invalid eagerness value */
__( 'The value "%s" is not a valid eagerness for a speculation rule.' ),
esc_html( $rule['eagerness'] )
),
'6.8.0'
);
return false;
}

if ( isset( $rule['where'] ) && 'immediate' === $rule['eagerness'] ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: forbidden eagerness value */
__( 'The eagerness value "%s" is forbidden for document-level speculation rules.' ),
'immediate'
),
'6.8.0'
);
return false;
}
}

if ( ! isset( $this->rules_by_mode[ $mode ] ) ) {
$this->rules_by_mode[ $mode ] = array();
}

$this->rules_by_mode[ $mode ][ $id ] = $rule;
return true;
}

/**
* Checks whether a speculation rule for the given mode and ID already exists.
*
* @since 6.8.0
*
* @param string $mode Speculative loading mode. Either 'prefetch' or 'prerender'.
* @param string $id Unique string identifier for the speculation rule.
* @return bool True if the rule already exists, false otherwise.
*/
public function has_rule( string $mode, string $id ): bool {
return isset( $this->rules_by_mode[ $mode ][ $id ] );
}

/**
* Returns the speculation rules data ready to be JSON-encoded.
*
* @since 6.8.0
*
* @return array<string, array<string, mixed>> Speculation rules data.
*/
#[ReturnTypeWillChange]
public function jsonSerialize() {
// Strip the IDs for JSON output, since they are not relevant for the Speculation Rules API.
return array_map(
static function ( array $rules ) {
return array_values( $rules );
},
array_filter( $this->rules_by_mode )
);
}

/**
* Checks whether the given ID is valid.
*
* @since 6.8.0
*
* @param string $id Unique string identifier for the speculation rule.
* @return bool True if the ID is valid, false otherwise.
*/
private function is_valid_id( string $id ): bool {
return (bool) preg_match( '/^[a-z][a-z0-9_-]+$/', $id );
}

/**
* Checks whether the given speculation rules mode is valid.
*
* @since 6.8.0
*
* @param string $mode Speculation rules mode.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_mode( string $mode ): bool {
return isset( self::$mode_allowlist[ $mode ] );
}

/**
* Checks whether the given speculation rules eagerness is valid.
*
* @since 6.8.0
*
* @param string $eagerness Speculation rules eagerness.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_eagerness( string $eagerness ): bool {
return isset( self::$eagerness_allowlist[ $eagerness ] );
}

/**
* Checks whether the given speculation rules source is valid.
*
* @since 6.8.0
*
* @param string $source Speculation rules source.
* @return bool True if valid, false otherwise.
*/
public static function is_valid_source( string $source ): bool {
return isset( self::$source_allowlist[ $source ] );
}
}
Loading
Loading