JezK
Edit File: patch-memory-leaks.php
<?php /** * Post-install patch for memory leak fixes in vendor-prefixed dependencies. * * Applies three patches: * A) DeclarationBlockParser: adds clearCache() static method * B) CssInliner: calls DeclarationBlockParser::clearCache() in clearAllCaches() * C) CssSelectorConverter: replaces unbounded cache with LRU-capped version * * Based on upstream fixes: * - https://github.com/symfony/symfony/pull/63400 * - https://github.com/MyIntervals/emogrifier/pull/1567 * * @package WooCommerce\EmailEditor */ $base = __DIR__ . '/../packages'; $patches = array(); // --- Patch A: DeclarationBlockParser — add clearCache() method --- $patches[] = array( 'file' => $base . '/Pelago/Emogrifier/Utilities/DeclarationBlockParser.php', 'marker' => 'public static function clearCache(): void', 'search' => ' private static $cache = []; /** * CSS custom properties (variables) have case-sensitive names, so their case must be preserved.', 'replace' => ' private static $cache = []; /** * Clears the static declaration block cache. * * This should be called between processing separate HTML documents to prevent * unbounded memory growth in long-running processes. */ public static function clearCache(): void { self::$cache = []; } /** * CSS custom properties (variables) have case-sensitive names, so their case must be preserved.', ); // --- Patch B: CssInliner — call DeclarationBlockParser::clearCache() --- $patches[] = array( 'file' => $base . '/Pelago/Emogrifier/CssInliner.php', 'marker' => 'DeclarationBlockParser::clearCache();', 'search' => ' private function clearAllCaches(): void { $this->caches = [ self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; }', 'replace' => ' private function clearAllCaches(): void { $this->caches = [ self::CACHE_KEY_SELECTOR => [], self::CACHE_KEY_COMBINED_STYLES => [], ]; DeclarationBlockParser::clearCache(); }', ); // --- Patch C: CssSelectorConverter — LRU cache --- $patches[] = array( 'file' => $base . '/Symfony/Component/CssSelector/CssSelectorConverter.php', 'marker' => 'maxCachedItems', 'search' => ' private $translator; private $cache; private static $xmlCache = []; private static $htmlCache = [];', 'replace' => ' private $translator; private $cache; /** * Maximum number of cached items per prefix before LRU eviction kicks in. * * @var int */ public static $maxCachedItems = 200; private static $xmlCache = []; private static $htmlCache = [];', ); $patches[] = array( 'file' => $base . '/Symfony/Component/CssSelector/CssSelectorConverter.php', 'marker' => 'array_key_first', 'search' => ' public function toXPath(string $cssExpr, string $prefix = \'descendant-or-self::\') { return $this->cache[$prefix][$cssExpr] ?? $this->cache[$prefix][$cssExpr] = $this->translator->cssToXPath($cssExpr, $prefix); }', 'replace' => ' public function toXPath(string $cssExpr, string $prefix = \'descendant-or-self::\') { if (isset($this->cache[$prefix][$cssExpr])) { // Promote to most-recently-used position. $value = $this->cache[$prefix][$cssExpr]; unset($this->cache[$prefix][$cssExpr]); return $this->cache[$prefix][$cssExpr] = $value; } $value = $this->translator->cssToXPath($cssExpr, $prefix); if (\count($this->cache[$prefix] ?? []) >= self::$maxCachedItems) { // Evict least-recently-used entry. unset($this->cache[$prefix][\array_key_first($this->cache[$prefix])]); } return $this->cache[$prefix][$cssExpr] = $value; }', ); $failed = false; foreach ( $patches as $patch ) { $name = basename( $patch['file'] ); if ( ! file_exists( $patch['file'] ) ) { echo "FAIL: File not found: {$patch['file']}\n"; $failed = true; continue; } $content = file_get_contents( $patch['file'] ); if ( strpos( $content, $patch['marker'] ) !== false ) { echo "SKIP: {$name} — already patched ({$patch['marker']})\n"; continue; } if ( strpos( $content, $patch['search'] ) === false ) { echo "FAIL: {$name} — search string not found. File may have changed upstream.\n"; $failed = true; continue; } $patched = str_replace( $patch['search'], $patch['replace'], $content ); file_put_contents( $patch['file'], $patched ); echo "OK: {$name} — patch applied\n"; } if ( $failed ) { echo "\nSome patches failed. Please check the output above.\n"; exit( 1 ); } echo "\nAll patches applied successfully.\n";