settings = \QWSpeedCache\Plugin::instance()->get_all_settings(); $this->cache_dir = QWSC_CACHE_DIR . 'css/'; if ( empty( $this->settings['minify_css'] ) && empty( $this->settings['combine_css'] ) ) { return; } if ( ! is_dir( $this->cache_dir ) ) { wp_mkdir_p( $this->cache_dir ); } add_action( 'wp_print_styles', array( $this, 'process' ), 999 ); } public function process() { if ( ! \QWSpeedCache\Plugin::should_optimize() || is_admin() || is_customize_preview() ) { return; } // Per-post "Disable minify/combine" is evaluated here, after the main query. if ( \QWSpeedCache\Admin\PostMetabox::flag( 'no_minify' ) ) { return; } global $wp_styles; if ( empty( $wp_styles->queue ) ) { return; } $to_process = array(); $excludes = $this->get_excludes(); foreach ( $wp_styles->queue as $handle ) { if ( empty( $wp_styles->registered[ $handle ] ) ) { continue; } $obj = $wp_styles->registered[ $handle ]; $src = isset( $obj->src ) ? (string) $obj->src : ''; if ( '' === $src ) { continue; } if ( \QWSpeedCache\Optimization\JsExclusions::is_excluded( $handle, $src, $excludes ) ) { continue; } if ( false !== strpos( $src, 'wp-includes' ) || false !== strpos( $src, 'wp-admin' ) ) { continue; } $to_process[ $handle ] = $obj; } if ( empty( $to_process ) ) { return; } if ( \QWSpeedCache\Optimization\CombineSafety::allow( $this->settings, 'css' ) ) { $to_combine = \QWSpeedCache\Optimization\CombineSafety::filter_items( $to_process, $this->settings, array( $this, 'get_local_path_public' ), 'css' ); if ( count( $to_combine ) >= 2 ) { $this->combine( $to_combine ); } } elseif ( ! empty( $this->settings['minify_css'] ) ) { $this->minify_individual( $to_process ); } } private function get_excludes() { return \QWSpeedCache\Optimization\JsExclusions::parse_list( $this->settings['exclude_css'] ?? '' ); } /** * Generate an asset safely: never replace a registered original if this * file is absent, empty or could not be written to the cache directory. * * @param string $path Cache file path. * @param string $data Generated asset bytes. * @return bool Whether the generated file is ready for use. */ private static function ensure_cache_asset( $path, $data ) { if ( ! is_string( $data ) || '' === $data ) { return false; } $expected = strlen( $data ); clearstatcache( true, $path ); if ( is_file( $path ) && is_readable( $path ) && filesize( $path ) === $expected ) { return true; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Generated local cache under wp-content/cache; verify the complete write. $written = @file_put_contents( $path, $data, LOCK_EX ); clearstatcache( true, $path ); return $written === $expected && is_file( $path ) && is_readable( $path ) && filesize( $path ) === $expected; } private function minify_individual( $items ) { global $wp_styles; foreach ( $items as $handle => $obj ) { $src = $this->get_local_path( $obj->src ); if ( ! $src || ! file_exists( $src ) ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a validated local enqueued CSS asset, never fetching a remote URL. $content = file_get_contents( $src ); if ( false === $content ) { continue; } $content = $this->rewrite_relative_urls( $content, $obj->src ); $min = $this->minify( $content ); $hash = substr( md5( $min ), 0, 12 ); // A registered asset handle is extension-controlled; never use it directly as a filesystem component. $safe_handle = sanitize_file_name( (string) $handle ); if ( '' === $safe_handle ) { $safe_handle = 'asset'; } $out = $this->cache_dir . $safe_handle . '-' . $hash . '.css'; if ( self::ensure_cache_asset( $out, $min ) ) { $wp_styles->registered[ $handle ]->src = $this->url_from_path( $out ); } } } private function combine( $items ) { global $wp_styles; $combined = ''; $handles = array(); foreach ( $items as $handle => $obj ) { $src = $this->get_local_path( $obj->src ); if ( ! $src || ! file_exists( $src ) ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a validated local enqueued CSS asset, never fetching a remote URL. $content = file_get_contents( $src ); if ( false === $content ) { continue; } $content = $this->rewrite_relative_urls( $content, $obj->src ); if ( ! empty( $this->settings['minify_css'] ) ) { $content = $this->minify( $content ); } $combined .= "/* {$handle} */\n" . $content . "\n"; $handles[] = $handle; } if ( empty( $combined ) ) { return; } $hash = substr( md5( $combined ), 0, 12 ); $out = $this->cache_dir . 'combined-' . $hash . '.css'; $was_ready = is_file( $out ) && is_readable( $out ) && filesize( $out ) === strlen( $combined ); if ( ! self::ensure_cache_asset( $out, $combined ) ) { return; // Originals are still queued and will load normally. } if ( ! $was_ready ) { do_action( 'qwsc_combined_css_written', $out ); } foreach ( $handles as $handle ) { wp_dequeue_style( $handle ); } wp_enqueue_style( 'qwsc-combined-css', $this->url_from_path( $out ), array(), QWSC_VERSION ); } /** * Keep relative CSS assets pointing to their original stylesheet location. * WordPress core, theme, custom uploads and relocated content paths can * have different filesystem and URL roots; use the registered source URL. * * @param string $css Original CSS. * @param string $source_url Registered stylesheet URL. * @return string */ private function rewrite_relative_urls( $css, $source_url ) { if ( ! is_string( $css ) || '' === $css || ! is_string( $source_url ) || '' === $source_url ) { return $css; } $source_url = trim( $source_url ); if ( 0 === strpos( $source_url, '//' ) ) { $source_url = ( is_ssl() ? 'https:' : 'http:' ) . $source_url; } elseif ( ! preg_match( '#^https?://#i', $source_url ) ) { if ( '/' === $source_url[0] ) { $site = wp_parse_url( home_url( '/' ) ); if ( ! is_array( $site ) || empty( $site['scheme'] ) || empty( $site['host'] ) ) { return $css; } $source_url = $site['scheme'] . '://' . $site['host'] . ( isset( $site['port'] ) ? ':' . $site['port'] : '' ) . $source_url; } else { global $wp_styles; $asset_base = ( isset( $wp_styles->base_url ) && is_string( $wp_styles->base_url ) ) ? $wp_styles->base_url : site_url( '/' ); $source_url = trailingslashit( $asset_base ) . ltrim( $source_url, '/' ); } } $parts = wp_parse_url( $source_url ); if ( ! is_array( $parts ) || empty( $parts['host'] ) || empty( $parts['scheme'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) { return $css; } $origin = $parts['scheme'] . '://' . $parts['host'] . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' ); $path = isset( $parts['path'] ) ? $parts['path'] : '/'; $base = $origin . trailingslashit( dirname( $path ) ); $resolve = static function ( $url ) use ( $origin, $base ) { $url = trim( $url ); if ( '' === $url || preg_match( '#^(?:[a-z][a-z0-9+.-]*:|//|\#|/|var\()#i', $url ) ) { return $url; } return $base . $url; }; // Preserve existing quotes, and do not alter absolute, data or fragment URLs. $css = preg_replace_callback( '/url\(\s*([\"\']?)([^)\"\']+)\1\s*\)/i', static function ( $m ) use ( $resolve ) { $url = trim( $m[2] ); $new = $resolve( $url ); return $new === $url ? $m[0] : 'url(' . $m[1] . $new . $m[1] . ')'; }, $css ); // CSS also permits @import "subdir/theme.css" without url(). return preg_replace_callback( '/@import\s+([\"\'])([^\"\']+)\1/i', static function ( $m ) use ( $resolve ) { return '@import ' . $m[1] . $resolve( $m[2] ) . $m[1]; }, $css ); } private function minify( $css ) { return \QWSpeedCache\Optimization\Minify\Engine::css( $css ); } public function get_local_path_public( $src ) { return $this->get_local_path( $src ); } private function get_local_path( $src ) { return LocalAssetPath::resolve( $src ); } private function url_from_path( $path ) { return content_url( 'cache/qw-speed-cache/css/' . basename( $path ) ); } }