From 28332b74759d85135fa4242f75824349ce766f83 Mon Sep 17 00:00:00 2001 From: Lester Caine Date: Sat, 6 Jun 2026 19:15:57 +0100 Subject: themes: fix APCu-poisoning of mRawFiles causing duplicate JS/CSS headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cleanAuxFiles() was converting mRawFiles filesystem paths to URL?filemtime strings in-place. Because mRawFiles is serialised into APCu, the URL form was persisted across requests. On the next request, isAuxFile(path) did in_array(path, [URL?timestamp]) — always a miss — so the file was added again at the next free position. Each request added another copy, growing the header indefinitely. Fix: cleanAuxFiles() now writes converted URLs into a new mRawUrls property (not in __sleep(), so never serialised) and leaves mRawFiles untouched as stable filesystem paths. html_head_inc.tpl reads mRawUrls for rendering. Also fixes a pre-existing strpos('?', $file) arg-order bug — the needle and haystack were swapped, so the ?/& separator was always wrong. Co-Authored-By: Claude Sonnet 4.6 --- includes/classes/BitThemes.php | 38 ++++++++++++++++++++++++++------------ templates/html_head_inc.tpl | 8 ++++---- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/includes/classes/BitThemes.php b/includes/classes/BitThemes.php index eb9c34d..1e21fc5 100755 --- a/includes/classes/BitThemes.php +++ b/includes/classes/BitThemes.php @@ -38,11 +38,19 @@ class BitThemes extends BitSingleton { 'css' => [], ]; - // Raw Javascript and Css Files + // Raw Javascript and Css Files — kept as filesystem paths for stable deduplication. + // Never mutated to URL form; see mRawUrls. public $mRawFiles = [ 'js' => [], 'css' => [], ]; + + // Per-request URL form of mRawFiles, built by cleanAuxFiles() for template rendering. + // Not serialised — rebuilt each request. + public $mRawUrls = [ + 'js' => [], + 'css' => [], + ]; public $mUnloadFiles = []; // Display Mode @@ -1729,24 +1737,30 @@ class BitThemes extends BitSingleton { } } - // convert full file path to URL in mRawFiles hash + // Build per-request mRawUrls — convert filesystem paths to URL?filemtime for rendering. + // mRawFiles is kept as stable paths so isAuxFile() deduplication survives APCu round-trips. + $this->mRawUrls[$pType] = []; if( !empty( $this->mRawFiles[$pType] )) { foreach( $this->mRawFiles[$pType] as $pos => $file ) { if ( KernelTools::is_windows() ) { - $file = str_replace( '\\', '/', $file ); - // Put first forward slash back - $file = substr_replace( $file, '\\', 2, 1 ); - $winBitRootPath = str_replace( '\\', '/', BIT_ROOT_PATH ); - // Put first forward slash back - $winBitRootPath = substr_replace($winBitRootPath, '\\', 2, 1 ); - if ( strpos( $file, $winBitRootPath ) !== false ) { - $this->mRawFiles[$pType][$pos] = BIT_ROOT_URL.substr( $file, strlen( $winBitRootPath )); + $winFile = str_replace( '\\', '/', $file ); + $winFile = substr_replace( $winFile, '\\', 2, 1 ); + $winBitRootPath = str_replace( '\\', '/', BIT_ROOT_PATH ); + $winBitRootPath = substr_replace( $winBitRootPath, '\\', 2, 1 ); + if ( strpos( $winFile, $winBitRootPath ) !== false ) { + $this->mRawUrls[$pType][$pos] = BIT_ROOT_URL.substr( $winFile, strlen( $winBitRootPath )); + } else { + $this->mRawUrls[$pType][$pos] = $file; } } else if ( strpos( $file, BIT_ROOT_PATH ) !== false ) { - $this->mRawFiles[$pType][$pos] = BIT_ROOT_URL.substr( $file, strlen( BIT_ROOT_PATH )); + $url = BIT_ROOT_URL.substr( $file, strlen( BIT_ROOT_PATH )); if( file_exists( $file ) && ($cacheTime = filemtime( $file )) ) { - $this->mRawFiles[$pType][$pos] .= (strpos('?',$file) ? '&' : '?' ).$cacheTime; + $url .= ( strpos( $url, '?' ) !== false ? '&' : '?' ).$cacheTime; } + $this->mRawUrls[$pType][$pos] = $url; + } else { + // Already a URL (e.g. CDN) — use as-is + $this->mRawUrls[$pType][$pos] = $file; } } } diff --git a/templates/html_head_inc.tpl b/templates/html_head_inc.tpl index 0c2c1cd..bb93d88 100755 --- a/templates/html_head_inc.tpl +++ b/templates/html_head_inc.tpl @@ -1,11 +1,11 @@ {strip} -{if !empty($gBitThemes->mRawFiles.css)} - {foreach from=$gBitThemes->mRawFiles.css item=cssFile} +{if !empty($gBitThemes->mRawUrls.css)} + {foreach from=$gBitThemes->mRawUrls.css item=cssFile} {/foreach} {/if} -{if !empty($gBitThemes->mRawFiles.js)} - {foreach from=$gBitThemes->mRawFiles.js item=jsFile} +{if !empty($gBitThemes->mRawUrls.js)} + {foreach from=$gBitThemes->mRawUrls.js item=jsFile} {/foreach} {/if} -- cgit v1.3