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( '~]*%s=(?["\']*)%s\k[^>]*>[\s\S]*?~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'; } }