tag to make it
* ignored by this class.
*
* @var string|null
*/
private $ignore_attribute;
/**
* HTML attribute value to be added to closing tag is encountered.
return array( '', $joint_buffer );
}
// No script tags detected, return both chunks unaltered.
return array( $buffer_start, $buffer_end );
}
// Makes sure all whole tags are in $buffer_start.
list( $buffer_start, $buffer_end ) = $this->recalculate_buffer_split( $joint_buffer, $script_tags );
foreach ( $script_tags as $script_tag ) {
$this->buffered_script_tags[] = $script_tag[0];
$buffer_start = str_replace( $script_tag[0], '', $buffer_start );
}
// Detect a lingering opened script.
$this->is_opened_script = $this->is_opened_script( $buffer_start . $buffer_end );
return array( $buffer_start, $buffer_end );
}
/**
* Matches ~i';
$result = preg_replace_callback(
$inline_script_regex,
function ( $script_match ) {
// Intentionally conservative: a simple case-insensitive substring check
// for "document.write" (which also covers "document.writeln"). It does
// not parse JS, so exotic call forms it cannot see — document['write'](),
// "document . write()", or an uppercase blocks so the counts below are symmetric.
$ignored_pair_regex = sprintf(
'~~si',
preg_quote( $this->ignore_attribute, '~' ),
preg_quote( $this->ignore_value, '~' )
);
$stripped = preg_replace( $ignored_pair_regex, '', $buffer );
if ( null === $stripped ) {
$stripped = $buffer;
}
// Strip HTML comments so a commented-out doesn't skew the count.
$stripped = preg_replace( '~~', '', $stripped ) ?? $stripped;
$opening_tags_count = preg_match_all( '~<\s*script(\s[^>]*)?>~i', $stripped );
$closing_tags_count = preg_match_all( '~<\s*/\s*script\s*>~i', $stripped );
return $opening_tags_count > $closing_tags_count;
}
/**
* Checks if the current request URL matches one of the exclusion patterns
* configured by the user.
*
* Runs at template_redirect time, when REQUEST_URI is available.
*
* @return bool
*/
private function is_current_request_excluded() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return false;
}
$patterns = function_exists( 'jetpack_boost_ds_get' ) ? jetpack_boost_ds_get( 'render_blocking_js_excludes' ) : array();
if ( empty( $patterns ) || ! is_array( $patterns ) ) {
return false;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only used for comparison.
return self::is_url_excluded( wp_unslash( $_SERVER['REQUEST_URI'] ), $patterns );
}
/**
* Checks whether a request URI matches any of the given exclusion patterns.
*
* Patterns follow the semantics documented for Page Cache bypass patterns:
* they are compared against the URL path (query strings are ignored),
* a `(.*)` or `*` wildcard matches any part of the path, trailing slashes
* are optional and the comparison is case-insensitive.
*
* Two things differ from Page Cache, so keep them in mind before unifying the
* two implementations: every character outside the wildcard tokens is escaped
* via preg_quote() and matched literally (a pattern like `page.html` never
* acts as a regular expression), and the path is percent-decoded so a pattern
* typed as it appears in the address bar matches an encoded request path.
*
* @param string $request_uri The request URI to check.
* @param array $patterns List of URL patterns.
*
* @return bool
*/
public static function is_url_excluded( $request_uri, $patterns ) {
$path = self::normalize_url_path( $request_uri );
foreach ( $patterns as $pattern ) {
$regex = self::get_exclusion_regex( $pattern );
if ( null === $regex ) {
continue;
}
$matched = preg_match( $regex, $path );
/*
* preg_match() returns false when PCRE cannot evaluate the pattern —
* e.g. a pathological pattern with several literal-separated wildcards
* hits the backtrack limit on a long URL. Treat that as a match so a
* deliberate exclusion is honoured (defer stays off on the page)
* rather than silently ignored.
*/
if ( 1 === $matched || false === $matched ) {
return true;
}
}
return false;
}
/**
* Extracts a normalized path from a URL or request URI.
*
* Drops the query string, ensures a leading slash and removes trailing
* slashes (except for the root path).
*
* @param string $url URL or request URI.
*
* @return string
*/
private static function normalize_url_path( $url ) {
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
// Decode percent-encoding so a pattern typed as it appears in the address
// bar (e.g. `foo bar`, or a non-ASCII slug) matches the encoded request
// path (`/foo%20bar`). Both the pattern and the request pass through here,
// so the two sides stay symmetric.
$path = rawurldecode( $path );
$path = '/' . ltrim( $path, '/' );
if ( '/' !== $path ) {
$path = rtrim( $path, '/' );
}
return self::strip_home_path( $path );
}
/**
* Removes the site's home directory prefix from a path.
*
* On a subdirectory install (e.g. a site at `/blog/`) the request URI
* includes the subdirectory but user-entered patterns generally do not.
* Stripping the home directory from both sides makes the comparison relative
* to the home root, so a `checkout` pattern matches `/blog/checkout`.
*
* @param string $path A normalized URL path (leading slash, no query/trailing slash).
*
* @return string
*/
private static function strip_home_path( $path ) {
$home_path = rtrim( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ), '/' );
if ( '' === $home_path ) {
return $path;
}
if ( 0 === strcasecmp( $path, $home_path ) ) {
return '/';
}
if ( 0 === strncasecmp( $path, $home_path . '/', strlen( $home_path ) + 1 ) ) {
return substr( $path, strlen( $home_path ) );
}
return $path;
}
/**
* Turns a single exclusion pattern into an anchored regular expression.
*
* @param mixed $pattern A user-provided URL pattern.
*
* @return string|null The regular expression, or null if the pattern is empty.
*/
private static function get_exclusion_regex( $pattern ) {
if ( ! is_string( $pattern ) ) {
return null;
}
$pattern = trim( $pattern );
if ( '' === $pattern ) {
return null;
}
/*
* Reject malformed URL patterns. A full URL with a scheme but no path
* (e.g. a typo'd `http://[::1`) would otherwise collapse to `/` and
* silently exclude only the homepage. A pathless URL that points at this
* site (e.g. the home URL pasted as `https://example.com`) is allowed
* through, since it legitimately means the homepage.
*/
$parsed = wp_parse_url( $pattern );
if ( false === $parsed ) {
return null;
}
if ( isset( $parsed['scheme'] ) && empty( $parsed['path'] ) ) {
$home_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
if ( empty( $parsed['host'] ) || 0 !== strcasecmp( $parsed['host'], (string) $home_host ) ) {
return null;
}
}
// Allow full URLs by stripping the home URL prefix (both secure and non-secure).
$home_url = home_url( '/' );
$pattern = str_ireplace(
array(
$home_url,
str_replace( 'http:', 'https:', $home_url ),
),
'/',
$pattern
);
$pattern = self::normalize_url_path( $pattern );
/*
* Split on wildcard tokens, treating any run of adjacent wildcards as a
* single split point. The possessive `++` is important: without coalescing,
* a pattern such as `****` would expand to one `.*` group per character, and
* thousands of wildcards would compile to thousands of groups and exhaust
* memory when matched against every front-end request. Possessive (rather
* than greedy `+`) keeps the split itself linear, so a pathological run of
* thousands of adjacent wildcards cannot exhaust the PCRE backtrack/JIT
* stack and make preg_split() return false.
*/
$tokens = preg_split( '/(?:\(\.\*\)|\(\*\)|\.\*|\*)++/', $pattern );
if ( false === $tokens ) {
return null;
}
// Everything between wildcards is matched literally; only the wildcards
// become a (non-capturing) `.*` group.
$quoted = array_map(
function ( $token ) {
return preg_quote( $token, '~' );
},
$tokens
);
return '~^' . implode( '(?:.*)', $quoted ) . '/?$~i';
}
public static function get_slug() {
return 'render_blocking_js';
}
}