Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .horde.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ dependencies:
horde/compress: ^3
horde/compress_fast: ^2
horde/controller: ^3
horde/cssminify: ^2
horde/cssminify: ^2.0.0-beta2
horde/data: ^3
horde/date: ^3
horde/exception: ^3
Expand Down
16 changes: 15 additions & 1 deletion lib/Horde/Themes/Css.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

use Horde\Log\Logger;

class Horde_Themes_Css
{
/**
Expand Down Expand Up @@ -253,8 +256,19 @@ public function getBaseStylesheetList()
*/
public function loadCssFiles($files)
{
global $injector;

$compress = new Horde_Themes_Css_Compress();
return $compress->compress($files);

// Try to get PSR-3 logger for error reporting
$logger = null;
try {
$logger = $injector->get(Logger::class);
} catch (Exception $e) {
// Logger not available, continue without logging
}

return $compress->compress($files, $logger);
}

}
15 changes: 13 additions & 2 deletions lib/Horde/Themes/Css/Cache/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* @package Core
*/

use Horde\Log\Logger;

/**
* Filesystem backend for the CSS caching library.
*
Expand All @@ -28,7 +30,7 @@ class Horde_Themes_Css_Cache_File extends Horde_Themes_Css_Cache
*/
public function process($css, $cacheid)
{
global $registry;
global $registry, $injector;

if (!empty($this->_params['filemtime'])) {
foreach ($css as &$val) {
Expand All @@ -49,8 +51,17 @@ public function process($css, $cacheid)

if (!file_exists($path)) {
$compress = new Horde_Themes_Css_Compress();

// Try to get PSR-3 logger for error reporting
$logger = null;
try {
$logger = $injector->get(Logger::class);
} catch (Exception $e) {
// Logger not available, continue without logging
}

$temp = Horde_Util::getTempFile('staticcss', true, $js_fs);
if (!file_put_contents($temp, $compress->compress($css), LOCK_EX) ||
if (!file_put_contents($temp, $compress->compress($css, $logger), LOCK_EX) ||
!chmod($temp, 0o777 & ~umask()) ||
!rename($temp, $path)) {
Horde::log('Could not write cached CSS file to disk.', 'EMERG');
Expand Down
13 changes: 12 additions & 1 deletion lib/Horde/Themes/Css/Cache/HordeCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* @package Core
*/

use Horde\Log\Logger;

/**
* Horde_Cache backend for the CSS caching library.
*
Expand Down Expand Up @@ -48,7 +50,16 @@ public function process($css, $cacheid)
// Do lifetime checking here, not on cache display page.
if (!$cache->exists($sig, empty($this->_params['lifetime']) ? 0 : $this->_params['lifetime'])) {
$compress = new Horde_Themes_Css_Compress();
$cache->set($sig, $compress->compress($css));

// Try to get PSR-3 logger for error reporting
$logger = null;
try {
$logger = $injector->get(Logger::class);
} catch (Exception $e) {
// Logger not available, continue without logging
}

$cache->set($sig, $compress->compress($css, $logger));
}

return [
Expand Down
68 changes: 57 additions & 11 deletions lib/Horde/Themes/Css/Compress.php
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
<?php

/**
* Copyright 2014-2017 Horde LLC (http://www.horde.org/)
* Copyright 2014-2026 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2014-2017 Horde LLC
* @copyright 2014-2026 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

use Horde\CssMinify\CssParserMinifier;
use Horde\CssMinify\ImportCallback;
use Horde\CssMinify\Input\CssFile;
use Horde\CssMinify\Input\FileCollectionInput;
use Horde\CssMinify\Settings;
use Horde\CssMinify\UrlCallback;
use Horde\Log\Logger;
use Psr\Log\LoggerInterface;

/**
* Compresses CSS based on Horde configuration parameters.
*
* @author Michael Slusarz <slusarz@horde.org>
* @category Horde
* @copyright 2014-2017 Horde LLC
* @copyright 2014-2026 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
* @since 2.12.0
Expand All @@ -29,25 +38,62 @@ class Horde_Themes_Css_Compress
* to a string.
*
* @param array $css See Horde_Themes_Css#getStylesheets().
* @param LoggerInterface|null $logger Optional PSR-3 logger for error reporting.
*
* @return string CSS data.
*/
public function compress($css)
public function compress($css, ?LoggerInterface $logger = null)
{
global $browser, $conf, $injector;

$files = [];
foreach ($css as $val) {
$files[$val['uri']] = $val['fs'];
if (!isset($val['uri']) || !isset($val['fs'])) {
continue;
}
try {
$files[] = new CssFile((string) $val['uri'], (string) $val['fs']);
} catch (InvalidArgumentException $e) {
// Skip unreadable files
if ($logger !== null) {
$logger->warning(
'Skipping unreadable CSS file: {file}',
['file' => $val['fs'] ?? 'unknown', 'exception' => $e->getMessage()]
);
}
continue;
}
}

if (empty($files)) {
return '';
}

$dataUrlCallback = null;
if (empty($conf['nobase64_img']) && $browser->hasFeature('dataurl')) {
$dataUrlCallback = new UrlCallback([$this, 'dataurlCallback']);
}

// Use provided logger or attempt to get from injector
if ($logger === null) {
try {
$logger = $injector->get(Logger::class);
} catch (Exception $e) {
// Logger not available, continue without logging
$logger = null;
}
}

$parser = new Horde_CssMinify_CssParser($files, [
'dataurl' => (empty($conf['nobase64_img']) && $browser->hasFeature('dataurl')) ? [$this, 'dataurlCallback'] : null,
'import' => [$this, 'importCallback'],
'logger' => $injector->getInstance('Horde_Log_Logger'),
]);
$minifier = new CssParserMinifier(
new FileCollectionInput(...$files),
new Settings(
dataUrlCallback: $dataUrlCallback,
importCallback: new ImportCallback([$this, 'importCallback']),
logger: $logger
)
);

return $parser->minify();
return $minifier->minify();
}

/**
Expand Down
145 changes: 145 additions & 0 deletions test/Horde_Themes_Css_CompressTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/**
* Tests for Horde_Themes_Css_Compress using modern CssMinify API.
*
* @category Horde
* @package Core
*/
class Horde_Themes_Css_CompressTest extends PHPUnit\Framework\TestCase
{
private string $fixturesPath;

protected function setUp(): void
{
$this->fixturesPath = __DIR__ . '/fixtures/';

// Create fixtures directory if it doesn't exist
if (!is_dir($this->fixturesPath)) {
mkdir($this->fixturesPath, 0o777, true);
}

// Create test CSS file
file_put_contents(
$this->fixturesPath . 'test.css',
'body { color: red; margin: 10px; }'
);

file_put_contents(
$this->fixturesPath . 'with-import.css',
'@import "shared.css"; body { background: white; }'
);

file_put_contents(
$this->fixturesPath . 'shared.css',
'.header { padding: 20px; }'
);

file_put_contents(
$this->fixturesPath . 'with-url.css',
'body { background-image: url(../images/bg.png); }'
);
}

protected function tearDown(): void
{
// Cleanup fixtures
$files = glob($this->fixturesPath . '*.css');
foreach ($files as $file) {
unlink($file);
}
if (is_dir($this->fixturesPath)) {
rmdir($this->fixturesPath);
}
}

public function testCompressBasicCss(): void
{
$compress = new Horde_Themes_Css_Compress();

$css = [
['uri' => 'test.css', 'fs' => $this->fixturesPath . 'test.css'],
];

$result = $compress->compress($css);

$this->assertStringContainsString('color:red', $result);
$this->assertStringContainsString('margin:10px', $result);
}

public function testCompressMultipleFiles(): void
{
$compress = new Horde_Themes_Css_Compress();

$css = [
['uri' => 'test.css', 'fs' => $this->fixturesPath . 'test.css'],
['uri' => 'shared.css', 'fs' => $this->fixturesPath . 'shared.css'],
];

$result = $compress->compress($css);

$this->assertStringContainsString('color:red', $result);
$this->assertStringContainsString('.header', $result);
}

public function testCompressHandlesInvalidFiles(): void
{
$compress = new Horde_Themes_Css_Compress();

$css = [
['uri' => 'missing.css', 'fs' => $this->fixturesPath . 'nonexistent.css'],
];

// Should not throw, returns empty string
$result = $compress->compress($css);

$this->assertSame('', $result);
}

public function testCompressHandlesMissingKeys(): void
{
$compress = new Horde_Themes_Css_Compress();

$css = [
['uri' => 'test.css'], // Missing 'fs'
['fs' => $this->fixturesPath . 'test.css'], // Missing 'uri'
[], // Missing both
];

// Should not throw, skips invalid entries
$result = $compress->compress($css);

$this->assertSame('', $result);
}

public function testCompressHandlesEmptyInput(): void
{
$compress = new Horde_Themes_Css_Compress();

$result = $compress->compress([]);

$this->assertSame('', $result);
}

public function testDataurlCallback(): void
{
$compress = new Horde_Themes_Css_Compress();

// Test callback is callable
$result = $compress->dataurlCallback('/path/to/image.png');

// Should return something (actual implementation depends on Horde_Themes_Image)
$this->assertIsString($result);
}

public function testImportCallback(): void
{
$compress = new Horde_Themes_Css_Compress();

// Test callback returns array with uri and fs
$result = $compress->importCallback('test.css');

$this->assertIsArray($result);
$this->assertCount(2, $result);
}
}
Loading
Loading