atch ) { // determine if it's a url() or an @import match. $type = ( strpos( $match[0], '@import' ) === 0 ? 'import' : 'url' ); $url = $match['path']; if ( preg_match( '/^#/', $url ) ) { continue; } if ( ! preg_match( '/^(data:|https?:|\\/)/', $url ) ) { // attempting to interpret GET-params makes no sense, so let's discard them for awhile. $params = strrchr( $url, '?' ); $url = $params ? substr( $url, 0, -strlen( $params ) ) : $url; // fix relative url. $url = filter_var( $source, FILTER_VALIDATE_URL ) ? dirname( $source ) . '/' . ltrim( $url, '/' ) : $converter->convert( $url ); // now that the path has been converted, re-apply GET-params. $url .= $params; } /* * Urls with control characters above 0x7e should be quoted. * According to Mozilla's parser, whitespace is only allowed at the * end of unquoted urls. * Urls with `)` (as could happen with data: uris) should also be * quoted to avoid being confused for the url() closing parentheses. * And urls with a # have also been reported to cause issues. * Urls with quotes inside should also remain escaped. * * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 * @see https://github.com/matthiasmullie/minify/issues/193 */ $url = trim( $url ); if ( preg_match( '/[\s\)\'"#\x{7f}-\x{9f}]/u', $url ) ) { $url = $match['quotes'] . $url . $match['quotes']; } // build replacement. $search[] = $match[0]; if ( 'url' === $type ) { $replace[] = 'url(' . $url . ')'; } elseif ( 'import' === $type ) { $replace[] = '@import "' . $url . '"'; } } // replace urls. return str_replace( $search, $replace, $content ); } /** * Replace local imports with their contents recursively. * * @since 3.8.6 * * @param string $content CSS Content. * @param string $target Target CSS file path. * * @return string */ protected function combine_imports( $content, $target ) { $import_regexes = [ // @import url(xxx) '/ # import statement @import # whitespace \s+ # open url() url\( # (optional) open path enclosure (?P["\']?) # fetch path (?P.+?) # (optional) close path enclosure (?P=quotes) # close url() \) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', // @import 'xxx' '/ # import statement @import # whitespace \s+ # open path enclosure (?P["\']) # fetch path (?P.+?) # close path enclosure (?P=quotes) # (optional) trailing whitespace \s* # (optional) media statement(s) (?P[^;]*) # (optional) trailing whitespace \s* # (optional) closing semi-colon ;? /ix', ]; // find all relative imports in css. $matches = []; foreach ( $import_regexes as $import_regexe ) { if ( preg_match_all( $import_regexe, $content, $regex_matches, PREG_SET_ORDER ) ) { $matches = array_merge( $matches, $regex_matches ); } } if ( empty( $matches ) ) { return $content; } $search = []; $replace = []; // loop the matches. foreach ( $matches as $match ) { /** * Filter Skip import replacement for one file. * * @since 3.8.6 * * @param bool Skipped or not (Default not skipped). * @param string $file_path Matched import path. * @param string $import_match Full import match. */ if ( apply_filters( 'rocket_skip_import_replacement', false, $match['path'], $match ) ) { continue; } list( $import_path, $import_content ) = $this->get_internal_file_contents( $match['path'], dirname( $target ) ); if ( empty( $import_content ) ) { continue; } if ( $this->check_cached_import( $import_path ) ) { $search[] = $match[0]; $replace[] = ''; continue; } $this->set_cached_import( $import_path ); // check if this is only valid for certain media. if ( ! empty( $match['media'] ) ) { $import_content = '@media ' . $match['media'] . '{' . $import_content . '}'; } // Use recursion to rewrite paths and combine imports again for imported content. $import_content = $this->rewrite_paths( $import_path, $target, $import_content ); // add to replacement array. $search[] = $match[0]; $replace[] = $import_content; } // replace the import statements. return str_replace( $search, $replace, $content ); } /** * Applies font-display:swap to all font-family rules without a previously set font-display property. * * @since 3.7 * * @param string $css_file_content CSS file content to modify. * * @return string Modified CSS content. */ private function apply_font_display_swap( $css_file_content ) { $css_file_content = (string) $css_file_content; return preg_replace_callback( '/(?:@font-face)\s*{(?[^}]+)}/i', function ( $matches ) { if ( preg_match( '/font-display:\s*(?\w*);?/i', $matches['value'], $attribute ) ) { if ( 'swap' === strtolower( $attribute['swap_value'] ) ) { return $matches[0]; } $swap = str_replace( $attribute['swap_value'], 'swap', $attribute[0] ); return preg_replace( '/font-display:\s*(?\w*);?/i', $swap, $matches[0] ); } return str_replace( $matches['value'], "font-display:swap;{$matches['value']}", $matches[0] ); }, $css_file_content ); } /** * Get internal file full path and contents. * * @since 3.8.6 * * @param string $file Internal file path (maybe external url or relative path). * @param string $base_path Base path as reference for relative paths. * * @return array Array of two values ( full path, contents ) */ private function get_internal_file_contents( $file, $base_path ) { if ( $this->is_external_path( $file ) && wp_http_validate_url( $file ) ) { return [ $file, false ]; } // Remove query strings. $file = str_replace( '?' . wp_parse_url( $file, PHP_URL_QUERY ), '', $file ); // Check if this file is readable or it's relative path so we add base_path at it's start. if ( ! rocket_direct_filesystem()->is_readable( $this->get_local_path( $file ) ) ) { $ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' ); $file = $base_path . $ds . str_replace( '/', $ds, $file ); }else { $file = $this->get_local_path( $file ); } $file_type = wp_check_filetype( $file, [ 'css' => 'text/css' ] ); if ( 'css' !== $file_type['ext'] ) { return [ $file, null ]; } $import_content = rocket_direct_filesystem()->get_contents( $file ); return [ $file, $import_content ]; } /** * Determines if the file is external. * * @since 3.8.6 * * @param string $url URL of the file. * @return bool True if external, false otherwise. */ protected function is_external_path( $url ) { $file = get_rocket_parse_url( $url ); if ( empty( $file['path'] ) ) { return true; } $parsed_site_url = wp_parse_url( site_url() ); if ( empty( $parsed_site_url['host'] ) ) { return true; } // This filter is documented in inc/Engine/Admin/Settings/Settings.php. $hosts = (array) apply_filters( 'rocket_cdn_hosts', [], [ 'all' ] ); $hosts[] = $parsed_site_url['host']; $langs = get_rocket_i18n_uri(); // Get host for all langs. foreach ( $langs as $lang ) { $url_host = wp_parse_url( $lang, PHP_URL_HOST ); if ( ! isset( $url_host ) ) { continue; } $hosts[] = $url_host; } $hosts = array_unique( $hosts ); if ( empty( $hosts ) ) { return true; } // URL has domain and domain is part of the internal domains. if ( ! empty( $file['host'] ) ) { foreach ( $hosts as $host ) { if ( false !== strpos( $url, $host ) ) { return false; } } return true; } return false; } /** * Get local absolute path for image. * * @since 3.8.6 * * @param string $url Image url. * * @return string Image absolute local path. */ private function get_local_path( $url ) { $url = $this->normalize_url( $url ); $path = rocket_url_to_path( $url ); if ( $path ) { return $path; } $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $ds = rocket_get_constant( 'DIRECTORY_SEPARATOR' ); $base_path = isset( $_SERVER['DOCUMENT_ROOT'] ) ? ( sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $ds ) : ''; return $base_path . str_replace( '/', $ds, $relative_url ); } /** * Normalize relative url to full url. * * @since 3.8.6 * * @param string $url Url to be normalized. * * @return string Normalized url. */ private function normalize_url( $url ) { $url_host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! empty( $url_host ) ) { return $url; } $relative_url = ltrim( wp_make_link_relative( $url ), '/' ); $site_url_components = wp_parse_url( site_url( '/' ) ); return $site_url_components['scheme'] . '://' . $site_url_components['host'] . '/' . $relative_url; } /** * Set cached import locally not to imported again. * * @param string $path Path to be cached. */ private function set_cached_import( string $path ) { $real_path = rocket_realpath( $path ); $this->imports[ md5( $real_path ) ] = $real_path; } /** * Check if path imported before. * * @param string $path Path to be checked. * * @return bool */ private function check_cached_import( string $path ): bool { return isset( $this->imports[ md5( rocket_realpath( $path ) ) ] ); } /** * Move charset to top of CSS file OR remove all charsets for internal CSS. * * @param string $content CSS content. * @param bool $keep_first_charset Keep first charset if true, otherwise remove all charsets. * * @return string */ public function handle_charsets( string $content, bool $keep_first_charset = true ): string { $new_content = preg_replace_callback( '/@charset\s+["|\'](.*?)["|\'];?/i', [ $this, 'match_charsets' ], $content ); if ( ! $keep_first_charset ) { return $new_content; } if ( is_null( $this->found_charset ) ) { return $content; } return "@charset \"{$this->found_charset}\";" . $new_content; } /** * Match each charset on the CSS file. * * @param array $match Match array. * * @return string */ private function match_charsets( array $match ): string { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.matchFound if ( is_null( $this->found_charset ) ) { $this->found_charset = $match[1]; } return ''; } }