From b3542d1d2de6e10ff0b647db34c2915ed7b60514 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Wed, 5 Feb 2025 07:18:10 -0500 Subject: [PATCH] Factor asset pipeline logic out of asset manager. --- module/VuFindTheme/Module.php | 1 + .../src/VuFindTheme/AssetPipeline.php | 471 ++++++++++++++++++ .../src/VuFindTheme/AssetPipelineFactory.php | 111 +++++ .../VuFindTheme/View/Helper/AssetManager.php | 422 +--------------- .../View/Helper/AssetManagerFactory.php | 7 +- 5 files changed, 594 insertions(+), 418 deletions(-) create mode 100644 module/VuFindTheme/src/VuFindTheme/AssetPipeline.php create mode 100644 module/VuFindTheme/src/VuFindTheme/AssetPipelineFactory.php diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index cccb70e144f..44220fccc65 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -87,6 +87,7 @@ public function getServiceConfig() ParentInjectTemplateListener::class => InjectTemplateListener::class, ], 'factories' => [ + AssetPipeline::class => AssetPipelineFactory::class, InjectTemplateListener::class => InjectTemplateListenerFactory::class, MixinGenerator::class => ThemeInfoInjectorFactory::class, Mobile::class => InvokableFactory::class, diff --git a/module/VuFindTheme/src/VuFindTheme/AssetPipeline.php b/module/VuFindTheme/src/VuFindTheme/AssetPipeline.php new file mode 100644 index 00000000000..f57b6cd07cf --- /dev/null +++ b/module/VuFindTheme/src/VuFindTheme/AssetPipeline.php @@ -0,0 +1,471 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFindTheme; + +use Exception; +use Laminas\Log\LoggerAwareInterface; +use Laminas\View\Helper\Url; +use MatthiasMullie\Minify\Minify; +use VuFind\Log\LoggerAwareTrait; +use VuFindTheme\View\Helper\RelativePathTrait; + +use function count; +use function defined; +use function in_array; +use function is_resource; + +/** + * Class to handle asset pipeline functionality. + * + * @category VuFind + * @package Theme + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class AssetPipeline implements LoggerAwareInterface +{ + use LoggerAwareTrait; + use RelativePathTrait; + + /** + * Map of asset types to minifier classes. + * + * @var array + */ + protected array $minifiers = [ + 'css' => \VuFindTheme\Minify\CSS::class, + 'js' => \MatthiasMullie\Minify\JS::class, + ]; + + /** + * Constructor + * + * @param ThemeInfo $themeInfo Theme information service + * @param Url $urlHelper URL view helper + * @param string|bool $pipelineConfig Config for current application environment + * @param ?int $maxImportSize Maximum imported (inlined) file size + */ + public function __construct( + protected ThemeInfo $themeInfo, + protected Url $urlHelper, + protected string|bool $pipelineConfig, + protected ?int $maxImportSize = null + ) { + } + + /** + * Check if the pipeline is functional. + * + * @return bool + */ + protected function isPipelineAvailable(): bool + { + try { + $cacheDir = $this->getResourceCacheDir(); + } catch (\Exception $e) { + $this->logError($e->getMessage()); + return false; + } + if ($cacheDir && !is_writable($cacheDir)) { + $this->logError("Cannot write to $cacheDir; disabling asset pipeline."); + return false; + } + return true; + } + + /** + * Check if config is enabled for the specified file type + * + * @param string $fileType File type to check for pipeline config + * + * @return bool + */ + protected function isPipelineEnabledForType(string $fileType): bool + { + $config = $this->pipelineConfig; + if ($config === false || $config == 'off' || $config == 'false' || $config === '0') { + return false; + } + if ($config == '*' || $config == 'on' || $config == 'true' || $config === true || $config === '1') { + return true; + } + $settings = array_map('trim', explode(',', $config)); + return in_array($fileType, $settings); + } + + /** + * Get the path to the directory where we can cache files generated by + * this trait. The directory will be created if it does not already exist. + * + * @return string + */ + protected function getResourceCacheDir(): string + { + if (!defined('LOCAL_CACHE_DIR')) { + throw new \Exception( + 'Asset pipeline feature depends on the LOCAL_CACHE_DIR constant.' + ); + } + // TODO: it might be better to use \VuFind\Cache\Manager here. + $cacheDir = LOCAL_CACHE_DIR . '/public/'; + if (!is_dir($cacheDir) && !file_exists($cacheDir)) { + if (!mkdir($cacheDir)) { + throw new \Exception("Unexpected problem creating cache directory: $cacheDir"); + } + } + return $cacheDir; + } + + /** + * Determine whether the asset is exempt from concatenation. + * + * @param array $item Asset + * @param string $type Type of asset (css or js) + * + * @return bool + * @throws Exception + */ + protected function isExcludedFromConcat(array $item, string $type): bool + { + if ($type === 'css') { + return !$this->isRelativePath($item['href']); + } elseif ($type === 'js') { + return empty($item['src']) + || !empty($item['attrs']['conditional']) + || !$this->isRelativePath($item['src']); + } + throw new Exception("Unknown type: $type"); + } + + /** + * Extract the file path from an asset. + * + * @param array $item Asset + * @param string $type Type of asset (css or js) + * + * @return string + * @throws Exception + */ + protected function getResourceFilePath(array $item, string $type): string + { + $key = $this->getFileKeyByType($type); + if (!isset($item[$key])) { + throw new Exception("Unexpected missing $key key in $type item."); + } + return $item[$key]; + } + + /** + * Get the group identification key for a specific asset. + * + * @param array $item Asset + * @param string $type Type of asset (css or js) + * + * @return string + * @throws Exception + */ + protected function getGroupType(array $item, string $type): string + { + if ($type === 'css') { + $groupType = $item['media'] ?? 'all'; + if (isset($item['conditionalStylesheet'])) { + $type .= '_' . $item['conditionalStylesheet']; + } + return $groupType; + } + return 'default'; + } + + /** + * Sort assets into groups that can be collapsed using a minifier. + * + * @param array $assets Assets to group + * @param string $type Type of assets (css or js) + * + * @return array + * @throws Exception + */ + protected function groupAssets(array $assets, string $type): array + { + $groups = []; + $groupTypeIndex = []; + + foreach ($assets as $item) { + if ($this->isExcludedFromConcat($item, $type)) { + $groups[] = [ + 'other' => true, + 'item' => $item, + ]; + continue; + } + + $path = $type . '/' . $this->getResourceFilePath($item, $type); + $details = $this->themeInfo->findContainingTheme( + $path, + ThemeInfo::RETURN_ALL_DETAILS + ); + // Deal with special case: $path was not found in any theme. + if (null === $details) { + $errorMsg = "Could not find file '$path' in theme files"; + $this->logError($errorMsg); + $groups[] = [ + 'other' => true, + 'item' => $item, + ]; + continue; + } + + $groupType = $this->getGroupType($item, $type); + $index = $groupTypeIndex[$groupType] ?? false; + if ($index === false) { + $groupTypeIndex[$groupType] = count($groups); + $groups[] = [ + 'items' => [$item], + 'key' => $details['path'] . filemtime($details['path']), + ]; + } elseif (!in_array($item, $groups[$index]['items'])) { + $groups[$index]['items'][] = $item; + $groups[$index]['key'] .= $details['path'] . filemtime($details['path']); + } + } + + return $groups; + } + + /** + * Check if a file is minifiable i.e. does not have a pattern that denotes it's + * already minified + * + * @param string $filename File name + * + * @return bool + */ + protected function isMinifiable(string $filename): bool + { + $basename = basename($filename); + return preg_match('/\.min\.(js|css)/', $basename) === 0; + } + + /** + * Get the minifier object for the specified file type. + * + * @param string $type Type of assets (css or js) + * + * @return Minify + * @throws Exception + */ + protected function getMinifier(string $type): Minify + { + $minifierClass = $this->minifiers[$type] ?? null; + if (!$minifierClass) { + throw new Exception("Unsupported type: $type"); + } + $minifier = new $minifierClass(); + if ($type === 'css' && null !== $this->maxImportSize) { + $minifier->setMaxImportSize($this->maxImportSize); + } + return $minifier; + } + + /** + * Get minified data for a file + * + * @param array $details File details + * @param string $concatPath Target path for the resulting file (used in minifier + * for path mapping) + * @param string $type Type of assets (css or js) + * + * @throws \Exception + * @return string + */ + protected function getMinifiedData(array $details, string $concatPath, string $type): string + { + if ($this->isMinifiable($details['path'])) { + $minifier = $this->getMinifier($type); + $minifier->add($details['path']); + $data = $minifier->execute($concatPath); + } else { + $data = file_get_contents($details['path']); + if (false === $data) { + throw new \Exception( + "Could not read file {$details['path']}" + ); + } + } + // Play it safe by terminating Javascript code with a semicolon + if ($type === 'js' && !str_ends_with(trim($data), ';')) { + $data .= ';'; + } + return $data; + } + + /** + * Create a concatenated file from the given group of files + * + * @param string $concatPath Resulting file path + * @param array $group Object containing 'key' and stdobj file 'items' + * @param string $type Type of assets (css or js) + * + * @throws \Exception + * @return void + */ + protected function createConcatenatedFile(string $concatPath, array $group, string $type): void + { + $data = []; + foreach ($group['items'] as $item) { + $details = $this->themeInfo->findContainingTheme( + $type . '/' . $this->getResourceFilePath($item, $type), + ThemeInfo::RETURN_ALL_DETAILS + ); + $details['path'] = realpath($details['path']); + $data[] = $this->getMinifiedData($details, $concatPath, $type); + } + // Separate each file's data with a new line so that e.g. a file + // ending in a comment doesn't cause the next one to also get commented out. + file_put_contents($concatPath, implode("\n", $data)); + } + + /** + * Using the concatKey, return the path of the concatenated file. + * Generate if it does not yet exist. + * + * @param array $group Grouped assets + * @param string $type Type of assets (css or js) + * + * @return string + */ + protected function getConcatenatedFilePath(array $group, string $type): string + { + // Don't recompress individual files + if (count($group['items']) === 1) { + $path = $this->getResourceFilePath($group['items'][0], $type); + $details = $this->themeInfo->findContainingTheme( + $type . '/' . $path, + ThemeInfo::RETURN_ALL_DETAILS + ); + return ($this->urlHelper)('home') . 'themes/' . $details['theme'] + . '/' . $type . '/' . $path; + } + // Locate/create concatenated asset file + $filename = md5($group['key']) . '.min.' . $type; + // Minifier uses realpath, so do that here too to make sure we're not + // pointing to a symlink. Otherwise the path converter won't find the correct + // shared directory part. + $concatPath = realpath($this->getResourceCacheDir()) . '/' . $filename; + if (!file_exists($concatPath)) { + $lockfile = "$concatPath.lock"; + $handle = fopen($lockfile, 'c+'); + if (!is_resource($handle)) { + throw new \Exception("Could not open lock file $lockfile"); + } + if (!flock($handle, LOCK_EX)) { + fclose($handle); + throw new \Exception("Could not lock file $lockfile"); + } + // Check again if file exists after acquiring the lock + if (!file_exists($concatPath)) { + try { + $this->createConcatenatedFile($concatPath, $group, $type); + } catch (\Exception $e) { + flock($handle, LOCK_UN); + fclose($handle); + throw $e; + } + } + flock($handle, LOCK_UN); + fclose($handle); + } + + return ($this->urlHelper)('home') . 'cache/' . $filename; + } + + /** + * Get the key name from the asset array where a filename/path can be set. + * + * @param string $type Type of assets (css or js) + * + * @return string + * @throws Exception + */ + protected function getFileKeyByType(string $type): string + { + $keys = ['css' => 'href', 'js' => 'src']; + if (isset($keys[$type])) { + return $keys[$type]; + } + throw new Exception("Unexpected type: $type"); + } + + /** + * Turn the output of groupAssets() into an array suitable for input to the view helpers. + * + * @param array $groups Grouped assets returned by groupAssets() + * @param string $type Type of assets (css or js) + * + * @return array + * @throws Exception + */ + protected function processGroupedAssets(array $groups, string $type): array + { + $assets = []; + + foreach ($groups as $group) { + if (isset($group['other'])) { + $assets[] = $group['item']; + } else { + $item = $group['items'][0]; + $item[$this->getFileKeyByType($type)] = $this->getConcatenatedFilePath($group, $type); + $assets[] = $item; + } + } + + return $assets; + } + + /** + * Process an array of assets through the pipeline. + * + * @param array $assets Assets to process + * @param string $type Type of assets (css or js) + * + * @return array + * @throws Exception + */ + public function process(array $assets, string $type): array + { + if (!$this->isPipelineEnabledForType($type) || !$this->isPipelineAvailable()) { + return $assets; + } + + $groupedAssets = $this->groupAssets($assets, $type); + return $this->processGroupedAssets($groupedAssets, $type); + } +} diff --git a/module/VuFindTheme/src/VuFindTheme/AssetPipelineFactory.php b/module/VuFindTheme/src/VuFindTheme/AssetPipelineFactory.php new file mode 100644 index 00000000000..ca53b35e6cb --- /dev/null +++ b/module/VuFindTheme/src/VuFindTheme/AssetPipelineFactory.php @@ -0,0 +1,111 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFindTheme; + +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface as ContainerException; +use Psr\Container\ContainerInterface; + +use function count; + +/** + * Factory for AssetPipeline class. + * + * @category VuFind + * @package Theme + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class AssetPipelineFactory implements FactoryInterface +{ + /** + * Split config and return prefixed setting with current environment. + * + * @param array $config Configuration settings + * + * @return string|bool + */ + protected function getPipelineConfig(array $config) + { + $default = false; + if (isset($config['Site']['asset_pipeline'])) { + $settings = array_map( + 'trim', + explode(';', $config['Site']['asset_pipeline']) + ); + foreach ($settings as $setting) { + $parts = array_map('trim', explode(':', $setting)); + if (APPLICATION_ENV === $parts[0]) { + return $parts[1]; + } elseif (count($parts) == 1) { + $default = $parts[0]; + } elseif ($parts[0] === '*') { + $default = $parts[1]; + } + } + } + return $default; + } + + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException&\Throwable if any other error occurs + */ + public function __invoke( + ContainerInterface $container, + $requestedName, + ?array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options sent to factory.'); + } + $configManager = $container->get(\VuFind\Config\PluginManager::class); + $config = $configManager->get('config')?->toArray() ?? []; + return new $requestedName( + $container->get(\VuFindTheme\ThemeInfo::class), + $container->get('ViewHelperManager')->get('url'), + $this->getPipelineConfig($config), + $config['Site']['asset_pipeline_max_css_import_size'] ?? null + ); + } +} diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 89e2cfc22bb..7cce177ab50 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -29,17 +29,9 @@ namespace VuFindTheme\View\Helper; -use Exception; -use Laminas\Log\LoggerAwareInterface; -use MatthiasMullie\Minify\Minify; -use VuFind\Log\LoggerAwareTrait; +use VuFindTheme\AssetPipeline; use VuFindTheme\ThemeInfo; -use function count; -use function defined; -use function in_array; -use function is_resource; - /** * Asset manager view helper (for pre-processing, combining when appropriate, etc.) * @@ -49,9 +41,8 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development Wiki */ -class AssetManager extends \Laminas\View\Helper\AbstractHelper implements LoggerAwareInterface +class AssetManager extends \Laminas\View\Helper\AbstractHelper { - use LoggerAwareTrait; use RelativePathTrait; /** @@ -78,59 +69,17 @@ class AssetManager extends \Laminas\View\Helper\AbstractHelper implements Logger /** * Constructor * - * @param ThemeInfo $themeInfo Theme information service - * @param string|bool $pipelineConfig Config for current application environment - * @param string $cspNonce Nonce from nonce generator (for content security policy) - * @param ?int $maxImportSize Maximum imported (inlined) file size + * @param ThemeInfo $themeInfo Theme information service + * @param AssetPipeline $pipeline Asset pipeline helper + * @param string $cspNonce Nonce from nonce generator (for content security policy) */ public function __construct( protected ThemeInfo $themeInfo, - protected string|bool $pipelineConfig, - protected string $cspNonce = '', - protected ?int $maxImportSize = null + protected AssetPipeline $pipeline, + protected string $cspNonce = '' ) { } - /** - * Check if the pipeline is functional. - * - * @return bool - */ - protected function isPipelineAvailable(): bool - { - try { - $cacheDir = $this->getResourceCacheDir(); - } catch (\Exception $e) { - $this->logError($e->getMessage()); - return false; - } - if ($cacheDir && !is_writable($cacheDir)) { - $this->logError("Cannot write to $cacheDir; disabling asset pipeline."); - return false; - } - return true; - } - - /** - * Check if config is enabled for the specified file type - * - * @param string $fileType File type to check for pipeline config - * - * @return bool - */ - protected function isPipelineEnabledForType(string $fileType): bool - { - $config = $this->pipelineConfig; - if ($config === false || $config == 'off' || $config == 'false' || $config === '0') { - return false; - } - if ($config == '*' || $config == 'on' || $config == 'true' || $config === true || $config === '1') { - return true; - } - $settings = array_map('trim', explode(',', $config)); - return in_array($fileType, $settings); - } - /** * Add raw CSS to the pipeline. * @@ -326,7 +275,7 @@ protected function outputScriptAssets($position): string { $output = []; $scriptHelper = $this->getView()->plugin('inlineScript'); - $processedScripts = $this->processForPipeline($this->scripts[$position], 'js'); + $processedScripts = $this->pipeline->process($this->scripts[$position], 'js'); foreach ($processedScripts as $i => $script) { if ($script['allowArbitraryAttrs'] ?? false) { $scriptHelper->setAllowArbitraryAttributes(true); @@ -346,359 +295,6 @@ protected function outputScriptAssets($position): string return implode("\n", $output); } - /** - * Get the path to the directory where we can cache files generated by - * this trait. The directory will be created if it does not already exist. - * - * @return string - */ - protected function getResourceCacheDir(): string - { - if (!defined('LOCAL_CACHE_DIR')) { - throw new \Exception( - 'Asset pipeline feature depends on the LOCAL_CACHE_DIR constant.' - ); - } - // TODO: it might be better to use \VuFind\Cache\Manager here. - $cacheDir = LOCAL_CACHE_DIR . '/public/'; - if (!is_dir($cacheDir) && !file_exists($cacheDir)) { - if (!mkdir($cacheDir)) { - throw new \Exception("Unexpected problem creating cache directory: $cacheDir"); - } - } - return $cacheDir; - } - - /** - * Determine whether the asset is exempt from concatenation. - * - * @param array $item Asset - * @param string $type Type of asset (css or js) - * - * @return bool - * @throws Exception - */ - protected function isExcludedFromConcat(array $item, string $type): bool - { - if ($type === 'css') { - return !$this->isRelativePath($item['href']); - } elseif ($type === 'js') { - return empty($item['src']) - || !empty($item['attrs']['conditional']) - || !$this->isRelativePath($item['src']); - } - throw new Exception("Unknown type: $type"); - } - - /** - * Extract the file path from an asset. - * - * @param array $item Asset - * @param string $type Type of asset (css or js) - * - * @return string - * @throws Exception - */ - protected function getResourceFilePath(array $item, string $type): string - { - $key = $this->getFileKeyByType($type); - if (!isset($item[$key])) { - throw new Exception("Unexpected missing $key key in $type item."); - } - return $item[$key]; - } - - /** - * Get the group identification key for a specific asset. - * - * @param array $item Asset - * @param string $type Type of asset (css or js) - * - * @return string - * @throws Exception - */ - protected function getGroupType(array $item, string $type): string - { - if ($type === 'css') { - $groupType = $item['media'] ?? 'all'; - if (isset($item['conditionalStylesheet'])) { - $type .= '_' . $item['conditionalStylesheet']; - } - return $groupType; - } - return 'default'; - } - - /** - * Sort assets into groups that can be collapsed using a minifier. - * - * @param array $assets Assets to group - * @param string $type Type of assets (css or js) - * - * @return array - * @throws Exception - */ - protected function groupAssets(array $assets, string $type): array - { - $groups = []; - $groupTypeIndex = []; - - foreach ($assets as $item) { - if ($this->isExcludedFromConcat($item, $type)) { - $groups[] = [ - 'other' => true, - 'item' => $item, - ]; - continue; - } - - $path = $type . '/' . $this->getResourceFilePath($item, $type); - $details = $this->themeInfo->findContainingTheme( - $path, - ThemeInfo::RETURN_ALL_DETAILS - ); - // Deal with special case: $path was not found in any theme. - if (null === $details) { - $errorMsg = "Could not find file '$path' in theme files"; - $this->logError($errorMsg); - $groups[] = [ - 'other' => true, - 'item' => $item, - ]; - continue; - } - - $groupType = $this->getGroupType($item, $type); - $index = $groupTypeIndex[$groupType] ?? false; - if ($index === false) { - $groupTypeIndex[$groupType] = count($groups); - $groups[] = [ - 'items' => [$item], - 'key' => $details['path'] . filemtime($details['path']), - ]; - } elseif (!in_array($item, $groups[$index]['items'])) { - $groups[$index]['items'][] = $item; - $groups[$index]['key'] .= $details['path'] . filemtime($details['path']); - } - } - - return $groups; - } - - /** - * Check if a file is minifiable i.e. does not have a pattern that denotes it's - * already minified - * - * @param string $filename File name - * - * @return bool - */ - protected function isMinifiable(string $filename): bool - { - $basename = basename($filename); - return preg_match('/\.min\.(js|css)/', $basename) === 0; - } - - /** - * Get the minifier object for the specified file type. - * - * @param string $type Type of assets (css or js) - * - * @return Minify - * @throws Exception - */ - protected function getMinifier(string $type): Minify - { - $minifier = match ($type) { - 'css' => new \VuFindTheme\Minify\CSS(), - 'js' => new \MatthiasMullie\Minify\JS(), - default => null - }; - if (!$minifier) { - throw new Exception("Unsupported type: $type"); - } - if ($type === 'css' && null !== $this->maxImportSize) { - $minifier->setMaxImportSize($this->maxImportSize); - } - return $minifier; - } - - /** - * Get minified data for a file - * - * @param array $details File details - * @param string $concatPath Target path for the resulting file (used in minifier - * for path mapping) - * @param string $type Type of assets (css or js) - * - * @throws \Exception - * @return string - */ - protected function getMinifiedData(array $details, string $concatPath, string $type): string - { - if ($this->isMinifiable($details['path'])) { - $minifier = $this->getMinifier($type); - $minifier->add($details['path']); - $data = $minifier->execute($concatPath); - } else { - $data = file_get_contents($details['path']); - if (false === $data) { - throw new \Exception( - "Could not read file {$details['path']}" - ); - } - } - // Play it safe by terminating Javascript code with a semicolon - if ($type === 'js' && !str_ends_with(trim($data), ';')) { - $data .= ';'; - } - return $data; - } - - /** - * Create a concatenated file from the given group of files - * - * @param string $concatPath Resulting file path - * @param array $group Object containing 'key' and stdobj file 'items' - * @param string $type Type of assets (css or js) - * - * @throws \Exception - * @return void - */ - protected function createConcatenatedFile(string $concatPath, array $group, string $type): void - { - $data = []; - foreach ($group['items'] as $item) { - $details = $this->themeInfo->findContainingTheme( - $type . '/' . $this->getResourceFilePath($item, $type), - ThemeInfo::RETURN_ALL_DETAILS - ); - $details['path'] = realpath($details['path']); - $data[] = $this->getMinifiedData($details, $concatPath, $type); - } - // Separate each file's data with a new line so that e.g. a file - // ending in a comment doesn't cause the next one to also get commented out. - file_put_contents($concatPath, implode("\n", $data)); - } - - /** - * Using the concatKey, return the path of the concatenated file. - * Generate if it does not yet exist. - * - * @param array $group Grouped assets - * @param string $type Type of assets (css or js) - * - * @return string - */ - protected function getConcatenatedFilePath(array $group, string $type): string - { - $urlHelper = $this->getView()->plugin('url'); - - // Don't recompress individual files - if (count($group['items']) === 1) { - $path = $this->getResourceFilePath($group['items'][0], $type); - $details = $this->themeInfo->findContainingTheme( - $type . '/' . $path, - ThemeInfo::RETURN_ALL_DETAILS - ); - return $urlHelper('home') . 'themes/' . $details['theme'] - . '/' . $type . '/' . $path; - } - // Locate/create concatenated asset file - $filename = md5($group['key']) . '.min.' . $type; - // Minifier uses realpath, so do that here too to make sure we're not - // pointing to a symlink. Otherwise the path converter won't find the correct - // shared directory part. - $concatPath = realpath($this->getResourceCacheDir()) . '/' . $filename; - if (!file_exists($concatPath)) { - $lockfile = "$concatPath.lock"; - $handle = fopen($lockfile, 'c+'); - if (!is_resource($handle)) { - throw new \Exception("Could not open lock file $lockfile"); - } - if (!flock($handle, LOCK_EX)) { - fclose($handle); - throw new \Exception("Could not lock file $lockfile"); - } - // Check again if file exists after acquiring the lock - if (!file_exists($concatPath)) { - try { - $this->createConcatenatedFile($concatPath, $group, $type); - } catch (\Exception $e) { - flock($handle, LOCK_UN); - fclose($handle); - throw $e; - } - } - flock($handle, LOCK_UN); - fclose($handle); - } - - return $urlHelper('home') . 'cache/' . $filename; - } - - /** - * Get the key name from the asset array where a filename/path can be set. - * - * @param string $type Type of assets (css or js) - * - * @return string - * @throws Exception - */ - protected function getFileKeyByType(string $type): string - { - $keys = ['css' => 'href', 'js' => 'src']; - if (isset($keys[$type])) { - return $keys[$type]; - } - throw new Exception("Unexpected type: $type"); - } - - /** - * Turn the output of groupAssets() into an array suitable for input to the view helpers. - * - * @param array $groups Grouped assets returned by groupAssets() - * @param string $type Type of assets (css or js) - * - * @return array - * @throws Exception - */ - protected function processGroupedAssets(array $groups, string $type): array - { - $assets = []; - - foreach ($groups as $group) { - if (isset($group['other'])) { - $assets[] = $group['item']; - } else { - $item = $group['items'][0]; - $item[$this->getFileKeyByType($type)] = $this->getConcatenatedFilePath($group, $type); - $assets[] = $item; - } - } - - return $assets; - } - - /** - * Process an array of assets through the pipeline. - * - * @param array $assets Assets to process - * @param string $type Type of assets (css or js) - * - * @return array - * @throws Exception - */ - protected function processForPipeline(array $assets, string $type): array - { - if (!$this->isPipelineEnabledForType($type) || !$this->isPipelineAvailable()) { - return $assets; - } - - $groupedAssets = $this->groupAssets($assets, $type); - return $this->processGroupedAssets($groupedAssets, $type); - } - /** * Return the HTML to output style assets. * @@ -707,7 +303,7 @@ protected function processForPipeline(array $assets, string $type): array protected function outputStyleAssets(): string { $headLink = $this->getView()->plugin('headLink'); - $processedStylesheets = $this->processForPipeline($this->stylesheets, 'css'); + $processedStylesheets = $this->pipeline->process($this->stylesheets, 'css'); foreach ($processedStylesheets as $sheet) { // Account for the theme system (when appropriate): if ($this->isRelativePath($sheet['href'])) { diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php index 6cc40db9b4b..8d6eef55497 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php @@ -99,15 +99,12 @@ public function __invoke( if (!empty($options)) { throw new \Exception('Unexpected options sent to factory.'); } - $configManager = $container->get(\VuFind\Config\PluginManager::class); $nonceGenerator = $container->get(\VuFind\Security\NonceGenerator::class); $nonce = $nonceGenerator->getNonce(); - $config = $configManager->get('config')?->toArray() ?? []; return new $requestedName( $container->get(\VuFindTheme\ThemeInfo::class), - $this->getPipelineConfig($config), - $nonce, - $config['Site']['asset_pipeline_max_css_import_size'] ?? null + $container->get(\VuFindTheme\AssetPipeline::class), + $nonce ); } }