diff --git a/lib/Horde/Themes/Css/Cache.php b/lib/Horde/Themes/Css/Cache.php index d1f033ba..3be984fd 100644 --- a/lib/Horde/Themes/Css/Cache.php +++ b/lib/Horde/Themes/Css/Cache.php @@ -54,6 +54,34 @@ public function __construct(array $params = []) */ abstract public function process($css, $cacheid); + /** + * Split off the stylesheets that are not local files (e.g. a CDN URL + * added by the cssfiles hook). These cannot be compressed, so they are + * linked as is, before the cached file. An absolute URL backed by a + * readable local file is kept in the cached file. + * + * @param array $css See Horde_Themes_Css#getStylesheets(). + * + * @return array Two elements: the list of external URLs (Horde_Url + * objects) and the list of local stylesheets. + */ + protected function _splitExternal($css) + { + $external = $local = []; + + foreach ($css as $val) { + if (isset($val['uri']) + && preg_match('#^https?://#i', (string) $val['uri']) + && !(isset($val['fs']) && is_readable($val['fs']))) { + $external[] = Horde::url($val['uri'], true, -1); + } else { + $local[] = $val; + } + } + + return [$external, $local]; + } + /** * Perform garbage collection. */ diff --git a/lib/Horde/Themes/Css/Cache/File.php b/lib/Horde/Themes/Css/Cache/File.php index 68bad5fa..e3906999 100644 --- a/lib/Horde/Themes/Css/Cache/File.php +++ b/lib/Horde/Themes/Css/Cache/File.php @@ -34,6 +34,11 @@ public function process($css, $cacheid) { global $registry, $injector; + [$external, $css] = $this->_splitExternal($css); + if (!count($css)) { + return $external; + } + if (!empty($this->_params['filemtime'])) { foreach ($css as &$val) { $val['mtime'] = @filemtime($val['fs']); @@ -71,9 +76,9 @@ public function process($css, $cacheid) } } - return [ + return array_merge($external, [ Horde::url(($registry->get('staticuri', 'horde') ?? '') . '/' . $filename, true, ['append_session' => -1]), - ]; + ]); } /** diff --git a/lib/Horde/Themes/Css/Cache/HordeCache.php b/lib/Horde/Themes/Css/Cache/HordeCache.php index 61eb90c9..91d04250 100644 --- a/lib/Horde/Themes/Css/Cache/HordeCache.php +++ b/lib/Horde/Themes/Css/Cache/HordeCache.php @@ -32,6 +32,11 @@ public function process($css, $cacheid) { global $injector; + [$external, $css] = $this->_splitExternal($css); + if (!count($css)) { + return $external; + } + if (!empty($this->_params['filemtime'])) { foreach ($css as &$val) { $val['mtime'] = @filemtime($val['fs']); @@ -62,9 +67,9 @@ public function process($css, $cacheid) $cache->set($sig, $compress->compress($css, $logger)); } - return [ + return array_merge($external, [ Horde::getCacheUrl('css', ['cid' => $sig]), - ]; + ]); } } diff --git a/test/Unit/Themes/Css/CacheTest.php b/test/Unit/Themes/Css/CacheTest.php new file mode 100644 index 00000000..db323e4c --- /dev/null +++ b/test/Unit/Themes/Css/CacheTest.php @@ -0,0 +1,82 @@ +createStub(Horde_Registry::class); + $GLOBALS['registry']->method('get')->willReturn('/horde'); + } + + protected function tearDown(): void + { + unset($GLOBALS['registry']); + } + + private function getCache(): Horde_Themes_Css_Cache + { + return new class extends Horde_Themes_Css_Cache { + public function process($css, $cacheid) + { + return $this->_splitExternal($css); + } + }; + } + + public function testSplitExternal(): void + { + $cdn = 'https://cdn.example.org/styles/main.css'; + $local = ['app' => 'horde', 'fs' => '/srv/themes/x/screen.css', 'uri' => '/themes/x/screen.css']; + + [$external, $rest] = $this->getCache()->process([ + ['app' => 'horde', 'fs' => $cdn, 'uri' => $cdn], + $local, + ], ''); + + $this->assertCount(1, $external); + $this->assertSame($cdn, $external[0]->toString(false, true)); + $this->assertSame([$local], $rest); + } + + public function testSplitExternalKeepsAbsoluteUrlWithLocalFile(): void + { + $fs = tempnam(sys_get_temp_dir(), 'css'); + $local = ['app' => 'horde', 'fs' => $fs, 'uri' => 'https://static.example.org/themes/x/screen.css']; + + try { + [$external, $rest] = $this->getCache()->process([$local], ''); + } finally { + unlink($fs); + } + + $this->assertSame([], $external); + $this->assertSame([$local], $rest); + } + + public function testSplitExternalWithoutExternal(): void + { + $local = ['app' => 'horde', 'fs' => '/srv/themes/x/screen.css', 'uri' => '/themes/x/screen.css']; + + [$external, $rest] = $this->getCache()->process([$local], ''); + + $this->assertSame([], $external); + $this->assertSame([$local], $rest); + } +}