# xspeed/1.1.7/includes/class-css-combine-buffer.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.1.7. 468 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.1.7/code/includes/class-css-combine-buffer.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.1.7/raw/includes/class-css-combine-buffer.php
- Modified: 2026-08-13T15:55:04+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.1.7/code/includes/class-css-combine-buffer.php#L10-L20`.

```php
<?php
/**
 * CSS combining on the finished HTML.
 *
 * The enqueue-stage combiner (Asset_Combiner::combine_styles) walked
 * WP_Styles->queue at priority 999 and rewrote handles: point one handle at the
 * merged file, blank the rest. That cannot be made correct, because WordPress
 * keeps editing the queue after we are done.
 *
 * The reported break (#195, WooCommerce + Kadence) was not a flaw in our
 * bucketing or carrier choice. Traced on a live install:
 *
 *   prio 998  kadence-global  src='.../global.min.css'
 *   prio 999  kadence-global  src=false          <- us, blanking a non-carrier
 *
 * ...and then core's `wp_maybe_inline_styles()` runs. It inlines any queued
 * handle carrying a `path` data key and sets `src = false` on it
 * (wp-includes/script-loader.php:3188). Our carrier was
 * `classic-theme-styles`, which core registers WITH a path — so core read that
 * handle's ORIGINAL file, inlined it, and discarded the combined URL we had
 * just written there. The merged <link> never printed and the five sheets we
 * had blanked were gone. Six stylesheets became one, and the site rendered
 * unstyled.
 *
 * No carrier-selection rule survives that: core rewrites the handle after us.
 * So combining moves to the finished HTML, where what we read is what shipped.
 * This is the layer LiteSpeed combines at, for the same reason.
 *
 * What that buys, beyond fixing the break:
 *
 *   - Document order is visible, so the cascade can be preserved exactly.
 *   - Sheets printed by plugins outside the queue are seen (they were
 *     invisible to a queue walker, and got duplicated).
 *   - `data-no-optimize` / `data-optimized` opt-outs work, matching what
 *     LiteSpeed and Autoptimize already honor.
 *   - The swap path is a pure string transform, so it is unit-testable —
 *     the enqueue version needed a full WP bootstrap and never had a test.
 *
 * The cascade rule: only CONTIGUOUS runs of same-media local sheets merge. A
 * sheet we cannot combine (external, opted out, excluded) ends the run, and
 * everything after it starts a new one. Nothing is ever hoisted past anything
 * else, which is the property the old combiner could not offer.
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

final class Css_Combine_Buffer {

	/** Minimum sheets in a run before merging is worth a request. */
	private const MIN_RUN = 2;

	/** Our own output-buffer nesting level, when we had to open one. */
	private static ?int $buffer_level = null;

	/** Set once we have transformed a page, so we never do it twice. */
	private static bool $done = false;

	/**
	 * Make sure SOMETHING will hand us the finished HTML.
	 *
	 * `xspeed_cache_final_html` is the preferred route — the page cache
	 * already buffers, so we transform once and the result is baked into the
	 * cache file. But that filter fires only on a cacheable MISS. With the
	 * page cache off, or on an excluded URL (`/cart`, `/checkout` — precisely
	 * where a WooCommerce layout break hurts most), it never fires at all and
	 * combining would silently stop working.
	 *
	 * So: open our own buffer when the cache is not going to give us one, and
	 * no-op when it is. `$done` guarantees a page is transformed once whichever
	 * path gets there first.
	 */
	public static function boot(): void {
		add_action(
			'template_redirect',
			static function (): void {
				if ( is_admin() || wp_doing_ajax() || wp_doing_cron()
					|| ( defined( 'REST_REQUEST' ) && REST_REQUEST )
					|| ( defined( 'WP_CLI' ) && WP_CLI )
					|| ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) {
					return;
				}
				// The page cache is buffering and will call us through its
				// filter; a second buffer would just copy the page again.
				if ( class_exists( '\\XSpeed\\Cache' ) && Cache::is_buffering() ) {
					return;
				}
				ob_start( array( __CLASS__, 'filter_buffer' ) );
				self::$buffer_level = ob_get_level();
				add_action( 'shutdown', array( __CLASS__, 'close_buffer' ), 0 );
			},
			1
		);
	}

	/** ob_start() callback — transform once, pass everything else through. */
	public static function filter_buffer( string $buffer ): string {
		return self::process( $buffer );
	}

	/**
	 * Clear the once-per-request guard.
	 *
	 * Only tests need this: a request is a fresh process, but a test run
	 * exercises many documents through one loaded class.
	 */
	public static function reset(): void {
		self::$done = false;
	}

	/** Flush only the buffer we opened. */
	public static function close_buffer(): void {
		if ( null !== self::$buffer_level && ob_get_level() >= self::$buffer_level ) {
			ob_end_flush();
			self::$buffer_level = null;
		}
	}

	/**
	 * Combine stylesheet links in a finished HTML document.
	 *
	 * Returns the input unchanged when there is nothing to gain, so a caller
	 * can hand us any page unconditionally.
	 *
	 * @param string $html Complete page HTML.
	 */
	public static function process( string $html ): string {
		if ( '' === $html || false === stripos( $html, '<link' ) ) {
			return $html;
		}
		// Both entry points can fire on one request (our buffer wraps the
		// page, the cache filter also runs). Transforming twice would be
		// harmless but wasteful — and would re-parse a document whose sheets
		// we already marked data-optimized.
		if ( self::$done ) {
			return $html;
		}

		// Only <head> is in scope. A <link> in the body is either a late
		// plugin injection or markup we do not own, and moving it changes
		// paint order for something that already chose to be there.
		$head_end = stripos( $html, '</head>' );
		if ( false === $head_end ) {
			return $html;
		}
		$head = substr( $html, 0, $head_end );

		$runs = self::runs( $head );
		if ( empty( $runs ) ) {
			return $html;
		}

		$new_head = $head;
		foreach ( $runs as $run ) {
			$merged = self::merge_run( $run );
			if ( null === $merged ) {
				continue;
			}
			// Replace the FIRST tag of the run with the combined link and drop
			// the rest. Reusing the first slot is what keeps the merged CSS
			// exactly where the earliest sheet was, preserving the cascade.
			$first = true;
			foreach ( $run['tags'] as $tag ) {
				$new_head = self::replace_once( $new_head, $tag, $first ? $merged : '' );
				$first    = false;
			}
		}

		if ( $new_head === $head ) {
			return $html;
		}
		self::$done = true;
		return $new_head . substr( $html, $head_end );
	}

	/**
	 * Split the head into contiguous runs of combinable same-media sheets.
	 *
	 * @return array<int,array{media:string,tags:string[],urls:string[]}>
	 */
	private static function runs( string $head ): array {
		// Blank out conditional comments and inline <style> so neither is
		// parsed into, and so an inline block BREAKS a run: it may carry
		// overrides that must keep their position between two sheets.
		$scan = self::mask( $head );

		if ( ! preg_match_all( '#<link\b[^>]*>#i', $scan, $m, PREG_OFFSET_CAPTURE ) ) {
			return array();
		}

		$excludes = self::excludes();
		$runs     = array();
		$open     = -1;      // index in $runs of the run still being extended.
		$prev_end = null;

		foreach ( $m[0] as $hit ) {
			$offset = (int) $hit[1];
			$tag    = substr( $head, $offset, strlen( (string) $hit[0] ) );

			if ( ! self::is_stylesheet( $tag ) ) {
				continue;
			}

			$url   = self::attr( $tag, 'href' );
			$media = self::media_of( $tag );
			$local = '' !== $url ? self::local_path( $url ) : null;

			$combinable = null !== $local
				&& ! self::opted_out( $tag )
				&& ! self::excluded( $url, $excludes );

			// Anything of substance BETWEEN two sheets ends the run: an inline
			// <style> or a conditional block may carry overrides whose position
			// relative to these sheets is load-bearing. Masked regions are NUL
			// in $scan, so their presence is the test.
			$gap = null === $prev_end ? '' : substr( $scan, $prev_end, $offset - $prev_end );
			// phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- testing whether the gap between two <link>s holds anything at all; wp_strip_all_tags() also trims and would hide a whitespace-only gap, which is exactly the case that must NOT break a run.
			$gap_breaks = '' !== $gap && ( false !== strpos( $gap, "\0" ) || '' !== trim( strip_tags( $gap ) ) );

			$prev_end = $offset + strlen( $tag );

			if ( ! $combinable ) {
				$open = -1;   // an uncombinable sheet ends the run it sits in.
				continue;
			}

			$extend = $open >= 0 && ! $gap_breaks && $runs[ $open ]['media'] === $media;
			if ( $extend ) {
				$runs[ $open ]['tags'][] = $tag;
				$runs[ $open ]['urls'][] = $local;
				continue;
			}

			$runs[] = array(
				'media' => $media,
				'tags'  => array( $tag ),
				'urls'  => array( $local ),
			);
			$open   = count( $runs ) - 1;
		}

		return array_values(
			array_filter(
				$runs,
				static fn( $r ) => count( $r['tags'] ) >= self::MIN_RUN
			)
		);
	}

	/**
	 * Build the combined file for one run and return its <link>, or null when
	 * nothing could be read.
	 *
	 * @param array{media:string,tags:string[],urls:string[]} $run Run to merge.
	 */
	private static function merge_run( array $run ): ?string {
		$key  = md5( implode( '|', array_map( static fn( $p ) => $p . ':' . (int) @filemtime( $p ), $run['urls'] ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing file contributes 0 to the key; handled below.
		$dir  = Asset_Combiner::cache_dir();
		$file = $dir . '/combined-' . $key . '.css';
		$url  = Asset_Combiner::cache_url() . '/combined-' . $key . '.css';

		if ( ! file_exists( $file ) ) {
			$css = '';
			foreach ( $run['urls'] as $path ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading a local stylesheet during page render; WP_Filesystem needs admin context.
				$body = (string) @file_get_contents( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- unreadable sheet is skipped, not fatal.
				if ( '' === $body ) {
					continue;
				}
				$src   = self::path_to_url( $path );
				$body  = self::strip_file_prelude( $body );
				$body  = Asset_Combiner::resolve_imports( $body, $src, 0 );
				$body  = Asset_Combiner::rewrite_url_paths( $body, $src );
				$css  .= "/* xspeed */\n" . $body . "\n";
			}
			if ( '' === trim( $css ) ) {
				return null;
			}
			$css = self::hoist_imports( $css );
			if ( ! is_dir( $dir ) ) {
				wp_mkdir_p( $dir );
			}
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents -- WP_Filesystem requires admin context, unavailable on the frontend.
			file_put_contents( $file, $css, LOCK_EX );
		}

		$media = 'all' === $run['media'] ? '' : sprintf( ' media="%s"', esc_attr( $run['media'] ) );

		// data-optimized marks it ours, so a second pass — or another
		// optimizer honoring the same convention — leaves it alone.
		// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- this REPLACES already-enqueued <link>s in the finished HTML; wp_enqueue_style() cannot run here (the page is rendered) and is the layer whose late rewrites caused #195.
		return sprintf(
			'<link rel="stylesheet" href="%s" data-optimized="1"%s />',
			esc_url( $url ),
			$media
		);
	}

	/* ------------------------------------------------------------------ */
	/* Parsing helpers                                                     */
	/* ------------------------------------------------------------------ */

	/**
	 * Replace conditional comments and inline <style> with NUL padding of the
	 * same length, so offsets still line up with the original string.
	 */
	private static function mask( string $head ): string {
		return (string) preg_replace_callback(
			'#<!--\[if.*?\[endif\]-->|<style\b[^>]*>.*?</style>|<!--.*?-->#is',
			static fn( $m ) => str_repeat( "\0", strlen( $m[0] ) ),
			$head
		);
	}

	private static function is_stylesheet( string $tag ): bool {
		return (bool) preg_match( '#\brel\s*=\s*["\']?stylesheet["\']?#i', $tag );
	}

	private static function attr( string $tag, string $name ): string {
		if ( preg_match( '#\b' . preg_quote( $name, '#' ) . '\s*=\s*["\']([^"\']*)["\']#i', $tag, $m ) ) {
			return trim( $m[1] );
		}
		return '';
	}

	/** '' and 'screen' both mean the on-screen document. */
	private static function media_of( string $tag ): string {
		$media = strtolower( self::attr( $tag, 'media' ) );
		return ( '' === $media || 'screen' === $media ) ? 'all' : $media;
	}

	private static function opted_out( string $tag ): bool {
		return (bool) preg_match( '#\bdata-(no-optimize|optimized)\b#i', $tag );
	}

	/** @return string[] */
	private static function excludes(): array {
		/**
		 * Filter: xspeed_combine_css_excludes
		 *
		 * Substrings matched against each stylesheet URL. A sheet that matches
		 * keeps its own <link> and breaks the run around it, so the cascade
		 * either side of it is untouched.
		 *
		 * @param string[] $excludes Substrings to leave alone.
		 */
		$list = apply_filters( 'xspeed_combine_css_excludes', array() );
		return is_array( $list ) ? array_filter( array_map( 'strval', $list ) ) : array();
	}

	/** @param string[] $excludes */
	private static function excluded( string $url, array $excludes ): bool {
		foreach ( $excludes as $needle ) {
			if ( '' !== $needle && false !== strpos( $url, $needle ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Absolute filesystem path for a same-origin stylesheet URL, or null when
	 * it is external, unreadable, or not a file we own.
	 */
	private static function local_path( string $url ): ?string {
		$url = trim( html_entity_decode( $url, ENT_QUOTES ) );
		if ( '' === $url || 0 === strpos( $url, 'data:' ) ) {
			return null;
		}
		$clean = strtok( $url, '?' );
		if ( false === $clean ) {
			return null;
		}
		$info = Asset_Combiner::local_info( Asset_Combiner::to_absolute_url( $clean ) );
		return is_array( $info ) && ! empty( $info['path'] ) ? (string) $info['path'] : null;
	}

	/** Inverse of local_path, for @import + url() resolution. */
	private static function path_to_url( string $path ): string {
		$root = defined( 'ABSPATH' ) ? rtrim( ABSPATH, '/' ) : '';
		if ( '' !== $root && 0 === strpos( $path, $root ) ) {
			return rtrim( home_url(), '/' ) . str_replace( $root, '', $path );
		}
		return $path;
	}

	/**
	 * Move any surviving `@import` to the top of the combined file.
	 *
	 * `resolve_imports()` inlines every import it can resolve, but a REMOTE
	 * one (a Google Fonts URL, a CDN stylesheet) cannot be inlined and is
	 * deliberately left in place. Standalone that is correct. In a combined
	 * file it lands mid-stream, and the CSS spec only honours `@import` before
	 * any style rule — so the browser silently drops it and that stylesheet
	 * never loads at all.
	 *
	 * Hoisting keeps them working. It does change their position relative to
	 * the merged rules, but an import that is ignored outright is strictly
	 * worse than one that loads early: ignored means the font or vendor sheet
	 * is simply absent.
	 */
	private static function hoist_imports( string $css ): string {
		if ( false === stripos( $css, '@import' ) ) {
			return $css;
		}

		$imports = array();
		$body    = (string) preg_replace_callback(
			'#@import\s+[^;]+;#i',
			static function ( $m ) use ( &$imports ) {
				$imports[] = trim( (string) $m[0] );
				return '';
			},
			$css
		);

		if ( empty( $imports ) ) {
			return $css;
		}
		// Preserve source order, and drop duplicates — the same font import
		// appearing in three merged sheets should be fetched once.
		return implode( "\n", array_unique( $imports ) ) . "\n" . $body;
	}

	/**
	 * Drop the bytes that are only legal at the START of a stylesheet.
	 *
	 * A UTF-8 BOM and an `@charset` rule are both position-sensitive: a
	 * browser strips a LEADING BOM and honours a FIRST-LINE `@charset`, but
	 * either one appearing mid-file is just a stray token — and it invalidates
	 * the rule immediately after it.
	 *
	 * Kadence ships `woocommerce.min.css` with a BOM (`ef bb bf`). Standalone
	 * that is fine. Concatenated third into a combined file it killed the rule
	 * that followed — `.kadence-shop-top-row`, the flex container for the
	 * WooCommerce shop toolbar — so "Showing all 4 results", the sorting
	 * dropdown and the grid/list toggles collapsed into three stacked rows on
	 * /shop, while every other page looked fine. (QA on #195)
	 *
	 * The combined file needs no `@charset` of its own: it is served with a
	 * `Content-Type: text/css` charset from the webserver, which outranks an
	 * in-file rule.
	 */
	private static function strip_file_prelude( string $css ): string {
		// BOM first — an @charset can sit behind one.
		if ( 0 === strncmp( $css, "\xEF\xBB\xBF", 3 ) ) {
			$css = substr( $css, 3 );
		}
		// Only a LEADING @charset is meaningful, so only that one is dropped;
		// the string "@charset" inside a rule or comment is left alone.
		return (string) preg_replace( '/^\s*@charset\s+["\'][^"\']*["\']\s*;/i', '', $css );
	}

	/** str_replace, but only the first occurrence. */
	private static function replace_once( string $haystack, string $needle, string $replace ): string {
		$pos = strpos( $haystack, $needle );
		if ( false === $pos ) {
			return $haystack;
		}
		return substr_replace( $haystack, $replace, $pos, strlen( $needle ) );
	}
}

```
