wp/wp-includes/class-wp-speculation-rules.php
changeset 22 8c2e4d02f4ef
equal deleted inserted replaced
21:48c4eec2b7e6 22:8c2e4d02f4ef
       
     1 <?php
       
     2 /**
       
     3  * Class 'WP_Speculation_Rules'.
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage Speculative Loading
       
     7  * @since 6.8.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Class representing a set of speculation rules.
       
    12  *
       
    13  * @since 6.8.0
       
    14  * @access private
       
    15  */
       
    16 final class WP_Speculation_Rules implements JsonSerializable {
       
    17 
       
    18 	/**
       
    19 	 * Stored rules, as a map of `$mode => $rules` pairs.
       
    20 	 *
       
    21 	 * Every `$rules` value is a map of `$id => $rule` pairs.
       
    22 	 *
       
    23 	 * @since 6.8.0
       
    24 	 * @var array<string, array<string, mixed>>
       
    25 	 */
       
    26 	private $rules_by_mode = array();
       
    27 
       
    28 	/**
       
    29 	 * The allowed speculation rules modes as a map, used for validation.
       
    30 	 *
       
    31 	 * @since 6.8.0
       
    32 	 * @var array<string, bool>
       
    33 	 */
       
    34 	private static $mode_allowlist = array(
       
    35 		'prefetch'  => true,
       
    36 		'prerender' => true,
       
    37 	);
       
    38 
       
    39 	/**
       
    40 	 * The allowed speculation rules eagerness levels as a map, used for validation.
       
    41 	 *
       
    42 	 * @since 6.8.0
       
    43 	 * @var array<string, bool>
       
    44 	 */
       
    45 	private static $eagerness_allowlist = array(
       
    46 		'immediate'    => true,
       
    47 		'eager'        => true,
       
    48 		'moderate'     => true,
       
    49 		'conservative' => true,
       
    50 	);
       
    51 
       
    52 	/**
       
    53 	 * The allowed speculation rules sources as a map, used for validation.
       
    54 	 *
       
    55 	 * @since 6.8.0
       
    56 	 * @var array<string, bool>
       
    57 	 */
       
    58 	private static $source_allowlist = array(
       
    59 		'list'     => true,
       
    60 		'document' => true,
       
    61 	);
       
    62 
       
    63 	/**
       
    64 	 * Adds a speculation rule to the speculation rules to consider.
       
    65 	 *
       
    66 	 * @since 6.8.0
       
    67 	 *
       
    68 	 * @param string               $mode Speculative loading mode. Either 'prefetch' or 'prerender'.
       
    69 	 * @param string               $id   Unique string identifier for the speculation rule.
       
    70 	 * @param array<string, mixed> $rule Associative array of rule arguments.
       
    71 	 * @return bool True on success, false if invalid parameters are provided.
       
    72 	 */
       
    73 	public function add_rule( string $mode, string $id, array $rule ): bool {
       
    74 		if ( ! self::is_valid_mode( $mode ) ) {
       
    75 			_doing_it_wrong(
       
    76 				__METHOD__,
       
    77 				sprintf(
       
    78 					/* translators: %s: invalid mode value */
       
    79 					__( 'The value "%s" is not a valid speculation rules mode.' ),
       
    80 					esc_html( $mode )
       
    81 				),
       
    82 				'6.8.0'
       
    83 			);
       
    84 			return false;
       
    85 		}
       
    86 
       
    87 		if ( ! $this->is_valid_id( $id ) ) {
       
    88 			_doing_it_wrong(
       
    89 				__METHOD__,
       
    90 				sprintf(
       
    91 					/* translators: %s: invalid ID value */
       
    92 					__( 'The value "%s" is not a valid ID for a speculation rule.' ),
       
    93 					esc_html( $id )
       
    94 				),
       
    95 				'6.8.0'
       
    96 			);
       
    97 			return false;
       
    98 		}
       
    99 
       
   100 		if ( $this->has_rule( $mode, $id ) ) {
       
   101 			_doing_it_wrong(
       
   102 				__METHOD__,
       
   103 				sprintf(
       
   104 					/* translators: %s: invalid ID value */
       
   105 					__( 'A speculation rule with ID "%s" already exists.' ),
       
   106 					esc_html( $id )
       
   107 				),
       
   108 				'6.8.0'
       
   109 			);
       
   110 			return false;
       
   111 		}
       
   112 
       
   113 		/*
       
   114 		 * Perform some basic speculation rule validation.
       
   115 		 * Every rule must have either a 'where' key or a 'urls' key, but not both.
       
   116 		 * The presence of a 'where' key implies a 'source' of 'document', while the presence of a 'urls' key implies
       
   117 		 * a 'source' of 'list'.
       
   118 		 */
       
   119 		if (
       
   120 			( ! isset( $rule['where'] ) && ! isset( $rule['urls'] ) ) ||
       
   121 			( isset( $rule['where'] ) && isset( $rule['urls'] ) )
       
   122 		) {
       
   123 			_doing_it_wrong(
       
   124 				__METHOD__,
       
   125 				sprintf(
       
   126 					/* translators: 1: allowed key, 2: alternative allowed key */
       
   127 					__( 'A speculation rule must include either a "%1$s" key or a "%2$s" key, but not both.' ),
       
   128 					'where',
       
   129 					'urls'
       
   130 				),
       
   131 				'6.8.0'
       
   132 			);
       
   133 			return false;
       
   134 		}
       
   135 		if ( isset( $rule['source'] ) ) {
       
   136 			if ( ! self::is_valid_source( $rule['source'] ) ) {
       
   137 				_doing_it_wrong(
       
   138 					__METHOD__,
       
   139 					sprintf(
       
   140 						/* translators: %s: invalid source value */
       
   141 						__( 'The value "%s" is not a valid source for a speculation rule.' ),
       
   142 						esc_html( $rule['source'] )
       
   143 					),
       
   144 					'6.8.0'
       
   145 				);
       
   146 				return false;
       
   147 			}
       
   148 
       
   149 			if ( 'list' === $rule['source'] && isset( $rule['where'] ) ) {
       
   150 				_doing_it_wrong(
       
   151 					__METHOD__,
       
   152 					sprintf(
       
   153 						/* translators: 1: source value, 2: forbidden key */
       
   154 						__( 'A speculation rule of source "%1$s" must not include a "%2$s" key.' ),
       
   155 						'list',
       
   156 						'where'
       
   157 					),
       
   158 					'6.8.0'
       
   159 				);
       
   160 				return false;
       
   161 			}
       
   162 
       
   163 			if ( 'document' === $rule['source'] && isset( $rule['urls'] ) ) {
       
   164 				_doing_it_wrong(
       
   165 					__METHOD__,
       
   166 					sprintf(
       
   167 						/* translators: 1: source value, 2: forbidden key */
       
   168 						__( 'A speculation rule of source "%1$s" must not include a "%2$s" key.' ),
       
   169 						'document',
       
   170 						'urls'
       
   171 					),
       
   172 					'6.8.0'
       
   173 				);
       
   174 				return false;
       
   175 			}
       
   176 		}
       
   177 
       
   178 		// If there is an 'eagerness' key specified, make sure it's valid.
       
   179 		if ( isset( $rule['eagerness'] ) ) {
       
   180 			if ( ! self::is_valid_eagerness( $rule['eagerness'] ) ) {
       
   181 				_doing_it_wrong(
       
   182 					__METHOD__,
       
   183 					sprintf(
       
   184 						/* translators: %s: invalid eagerness value */
       
   185 						__( 'The value "%s" is not a valid eagerness for a speculation rule.' ),
       
   186 						esc_html( $rule['eagerness'] )
       
   187 					),
       
   188 					'6.8.0'
       
   189 				);
       
   190 				return false;
       
   191 			}
       
   192 
       
   193 			if ( isset( $rule['where'] ) && 'immediate' === $rule['eagerness'] ) {
       
   194 				_doing_it_wrong(
       
   195 					__METHOD__,
       
   196 					sprintf(
       
   197 						/* translators: %s: forbidden eagerness value */
       
   198 						__( 'The eagerness value "%s" is forbidden for document-level speculation rules.' ),
       
   199 						'immediate'
       
   200 					),
       
   201 					'6.8.0'
       
   202 				);
       
   203 				return false;
       
   204 			}
       
   205 		}
       
   206 
       
   207 		if ( ! isset( $this->rules_by_mode[ $mode ] ) ) {
       
   208 			$this->rules_by_mode[ $mode ] = array();
       
   209 		}
       
   210 
       
   211 		$this->rules_by_mode[ $mode ][ $id ] = $rule;
       
   212 		return true;
       
   213 	}
       
   214 
       
   215 	/**
       
   216 	 * Checks whether a speculation rule for the given mode and ID already exists.
       
   217 	 *
       
   218 	 * @since 6.8.0
       
   219 	 *
       
   220 	 * @param string $mode Speculative loading mode. Either 'prefetch' or 'prerender'.
       
   221 	 * @param string $id   Unique string identifier for the speculation rule.
       
   222 	 * @return bool True if the rule already exists, false otherwise.
       
   223 	 */
       
   224 	public function has_rule( string $mode, string $id ): bool {
       
   225 		return isset( $this->rules_by_mode[ $mode ][ $id ] );
       
   226 	}
       
   227 
       
   228 	/**
       
   229 	 * Returns the speculation rules data ready to be JSON-encoded.
       
   230 	 *
       
   231 	 * @since 6.8.0
       
   232 	 *
       
   233 	 * @return array<string, array<string, mixed>> Speculation rules data.
       
   234 	 */
       
   235 	#[ReturnTypeWillChange]
       
   236 	public function jsonSerialize() {
       
   237 		// Strip the IDs for JSON output, since they are not relevant for the Speculation Rules API.
       
   238 		return array_map(
       
   239 			static function ( array $rules ) {
       
   240 				return array_values( $rules );
       
   241 			},
       
   242 			array_filter( $this->rules_by_mode )
       
   243 		);
       
   244 	}
       
   245 
       
   246 	/**
       
   247 	 * Checks whether the given ID is valid.
       
   248 	 *
       
   249 	 * @since 6.8.0
       
   250 	 *
       
   251 	 * @param string $id Unique string identifier for the speculation rule.
       
   252 	 * @return bool True if the ID is valid, false otherwise.
       
   253 	 */
       
   254 	private function is_valid_id( string $id ): bool {
       
   255 		return (bool) preg_match( '/^[a-z][a-z0-9_-]+$/', $id );
       
   256 	}
       
   257 
       
   258 	/**
       
   259 	 * Checks whether the given speculation rules mode is valid.
       
   260 	 *
       
   261 	 * @since 6.8.0
       
   262 	 *
       
   263 	 * @param string $mode Speculation rules mode.
       
   264 	 * @return bool True if valid, false otherwise.
       
   265 	 */
       
   266 	public static function is_valid_mode( string $mode ): bool {
       
   267 		return isset( self::$mode_allowlist[ $mode ] );
       
   268 	}
       
   269 
       
   270 	/**
       
   271 	 * Checks whether the given speculation rules eagerness is valid.
       
   272 	 *
       
   273 	 * @since 6.8.0
       
   274 	 *
       
   275 	 * @param string $eagerness Speculation rules eagerness.
       
   276 	 * @return bool True if valid, false otherwise.
       
   277 	 */
       
   278 	public static function is_valid_eagerness( string $eagerness ): bool {
       
   279 		return isset( self::$eagerness_allowlist[ $eagerness ] );
       
   280 	}
       
   281 
       
   282 	/**
       
   283 	 * Checks whether the given speculation rules source is valid.
       
   284 	 *
       
   285 	 * @since 6.8.0
       
   286 	 *
       
   287 	 * @param string $source Speculation rules source.
       
   288 	 * @return bool True if valid, false otherwise.
       
   289 	 */
       
   290 	public static function is_valid_source( string $source ): bool {
       
   291 		return isset( self::$source_allowlist[ $source ] );
       
   292 	}
       
   293 }