From 53bbfc2664352899a790546054b5e9b5c56da2f9 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 28 Jan 2025 14:58:46 -0500 Subject: [PATCH 01/30] Refactor headLink calls to assetPipeline helper. --- module/VuFindTheme/Module.php | 35 +++---- .../VuFindTheme/View/Helper/AssetPipeline.php | 95 +++++++++++++++++++ .../View/Helper/SetupThemeResources.php | 5 +- .../templates/Recommend/MapSelection.phtml | 10 +- .../Recommend/SideFacets/range-slider.phtml | 2 +- .../Recommend/SideFacetsDeferred.phtml | 2 +- .../templates/RecordDriver/EDS/core.phtml | 2 +- .../RecordDriver/EDS/result-list.phtml | 2 +- .../templates/RecordDriver/EPF/core.phtml | 2 +- .../bootstrap5/templates/RecordTab/map.phtml | 2 +- .../templates/channels/channelList.phtml | 4 +- .../templates/combined/results.phtml | 2 +- .../bootstrap5/templates/layout/layout.phtml | 4 +- .../templates/search/advanced/eds.phtml | 2 +- .../templates/search/advanced/ranges.phtml | 2 +- .../templates/search/searchbox.phtml | 2 +- .../root/templates/Helpers/icons/font.phtml | 2 +- themes/root/templates/layout/help.phtml | 2 +- 18 files changed, 132 insertions(+), 45 deletions(-) create mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/AssetPipeline.php diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index 5f9edd01279..7a8f5238002 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -87,8 +87,7 @@ public function getServiceConfig() ParentInjectTemplateListener::class => InjectTemplateListener::class, ], 'factories' => [ - InjectTemplateListener::class => - InjectTemplateListenerFactory::class, + InjectTemplateListener::class => InjectTemplateListenerFactory::class, MixinGenerator::class => ThemeInfoInjectorFactory::class, Mobile::class => InvokableFactory::class, ResourceContainer::class => InvokableFactory::class, @@ -108,34 +107,26 @@ public function getViewHelperConfig() { return [ 'factories' => [ - View\Helper\FootScript::class => - View\Helper\PipelineInjectorFactory::class, + View\Helper\AssetPipeline::class => InvokableFactory::class, + View\Helper\FootScript::class => View\Helper\PipelineInjectorFactory::class, View\Helper\ImageLink::class => View\Helper\ImageLinkFactory::class, - View\Helper\HeadLink::class => - View\Helper\PipelineInjectorFactory::class, - View\Helper\HeadScript::class => - View\Helper\PipelineInjectorFactory::class, - View\Helper\ParentTemplate::class => - View\Helper\ParentTemplateFactory::class, - View\Helper\InlineScript::class => - View\Helper\PipelineInjectorFactory::class, - View\Helper\Slot::class => - View\Helper\PipelineInjectorFactory::class, - View\Helper\TemplatePath::class => - View\Helper\TemplatePathFactory::class, - View\Helper\SetupThemeResources::class => - View\Helper\SetupThemeResourcesFactory::class, + View\Helper\HeadLink::class => View\Helper\PipelineInjectorFactory::class, + View\Helper\HeadScript::class => View\Helper\PipelineInjectorFactory::class, + View\Helper\ParentTemplate::class => View\Helper\ParentTemplateFactory::class, + View\Helper\InlineScript::class => View\Helper\PipelineInjectorFactory::class, + View\Helper\Slot::class => View\Helper\PipelineInjectorFactory::class, + View\Helper\TemplatePath::class => View\Helper\TemplatePathFactory::class, + View\Helper\SetupThemeResources::class => View\Helper\SetupThemeResourcesFactory::class, ], 'aliases' => [ + 'assetPipeline' => View\Helper\AssetPipeline::class, 'footScript' => View\Helper\FootScript::class, // Legacy alias for compatibility with pre-8.0 templates: 'headThemeResources' => View\Helper\SetupThemeResources::class, 'imageLink' => View\Helper\ImageLink::class, \Laminas\View\Helper\HeadLink::class => View\Helper\HeadLink::class, - \Laminas\View\Helper\HeadScript::class => - View\Helper\HeadScript::class, - \Laminas\View\Helper\InlineScript::class => - View\Helper\InlineScript::class, + \Laminas\View\Helper\HeadScript::class => View\Helper\HeadScript::class, + \Laminas\View\Helper\InlineScript::class => View\Helper\InlineScript::class, 'parentTemplate' => View\Helper\ParentTemplate::class, 'slot' => View\Helper\Slot::class, 'templatePath' => View\Helper\TemplatePath::class, diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetPipeline.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetPipeline.php new file mode 100644 index 00000000000..7f627bc83a1 --- /dev/null +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetPipeline.php @@ -0,0 +1,95 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace VuFindTheme\View\Helper; + +/** + * Asset pipeline view helper. + * + * @category VuFind + * @package View_Helpers + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class AssetPipeline extends \Laminas\View\Helper\AbstractHelper +{ + /** + * Add an entry to the list of stylesheets. + * + * @param string $href Stylesheet href + * + * @return void + */ + public function appendStylesheet(string $href): void + { + $this->getView()->plugin('headLink')->appendStylesheet($href); + } + + /** + * Forcibly prepend a stylesheet, removing it from any existing position + * + * @param string $href Stylesheet href + * @param string $media Media + * @param string $conditionalStylesheet Any conditions + * @param array $extras Array of extra attributes + * + * @return void + */ + public function forcePrependStylesheet( + string $href, + string $media = 'screen', + string $conditionalStylesheet = '', + array $extras = [] + ): void { + $this->getView()->plugin('headLink')->forcePrependStylesheet($href, $media, $conditionalStylesheet, $extras); + } + + /** + * Clear the list of stylesheets, re-establishing it with the provided one. + * + * @param string $href Stylesheet href + * + * @return void + */ + public function setStylesheet(string $href): void + { + $this->getView()->plugin('headLink')->setStylesheet($href); + } + + /** + * Output the collected assets. + * + * @return string + */ + public function outputAssets(): string + { + return ($this->getView()->plugin('headLink'))(); + } +} diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/SetupThemeResources.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/SetupThemeResources.php index 5809fee5462..9e47cda271b 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/SetupThemeResources.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/SetupThemeResources.php @@ -109,12 +109,12 @@ protected function addMetaTags() protected function addLinks(bool $partial = false) { // Convenient shortcut to view helper: - $headLink = $this->getView()->plugin('headLink'); + $assetPipeline = $this->getView()->plugin('assetPipeline'); // Load CSS (make sure we prepend them in the appropriate order; theme // resources should load before extras added by individual templates): foreach (array_reverse($this->container->getCss()) as $current) { - $headLink()->forcePrependStylesheet( + $assetPipeline->forcePrependStylesheet( $current['file'], empty($current['media']) ? 'all' : $current['media'], $current['conditional'] ?? '', @@ -128,6 +128,7 @@ protected function addLinks(bool $partial = false) // a link element for each. // Skip favicons in partial mode because they are illegal outside of . if (!$partial && ($favicon = $this->container->getFavicon())) { + $headLink = $this->getView()->plugin('headLink'); $imageLink = $this->getView()->plugin('imageLink'); if (is_array($favicon)) { foreach ($favicon as $attrs) { diff --git a/themes/bootstrap5/templates/Recommend/MapSelection.phtml b/themes/bootstrap5/templates/Recommend/MapSelection.phtml index 1566cf53fdd..553266d49c3 100644 --- a/themes/bootstrap5/templates/Recommend/MapSelection.phtml +++ b/themes/bootstrap5/templates/Recommend/MapSelection.phtml @@ -9,11 +9,11 @@ $this->headScript()->appendFile('vendor/leaflet/leaflet.draw.js'); $this->headScript()->appendFile('vendor/leaflet/leaflet.markercluster.js'); $this->headScript()->appendFile('map_selection_leaflet.js'); - $this->headLink()->appendStylesheet('vendor/leaflet/leaflet.css'); - $this->headLink()->appendStylesheet('vendor/leaflet/leaflet.draw.css'); - $this->headLink()->appendStylesheet('vendor/leaflet/MarkerCluster.css'); - $this->headLink()->appendStylesheet('vendor/leaflet/MarkerCluster.Default.css'); - $this->headLink()->appendStylesheet('geofeatures.css'); + $this->assetPipeline()->appendStylesheet('vendor/leaflet/leaflet.css'); + $this->assetPipeline()->appendStylesheet('vendor/leaflet/leaflet.draw.css'); + $this->assetPipeline()->appendStylesheet('vendor/leaflet/MarkerCluster.css'); + $this->assetPipeline()->appendStylesheet('vendor/leaflet/MarkerCluster.Default.css'); + $this->assetPipeline()->appendStylesheet('geofeatures.css'); $basemap = $this->recommend->getBasemap(); $geoField = $this->recommend->getGeoField(); diff --git a/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml b/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml index 4aa1b7fc9c1..a08c3939585 100644 --- a/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml +++ b/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml @@ -36,7 +36,7 @@ facet['type'] == 'date'): ?> headScript()->appendFile('vendor/bootstrap-slider.min.js'); ?> - headLink()->appendStylesheet('vendor/bootstrap-slider.min.css'); ?> + assetPipeline()->appendStylesheet('vendor/bootstrap-slider.min.css'); ?> $facetName) { if (isset($rangeFacets[$field]) && 'date' === $rangeFacets[$field]['type']) { $this->headScript()->appendFile('vendor/bootstrap-slider.min.js'); - $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css'); + $this->assetPipeline()->appendStylesheet('vendor/bootstrap-slider.min.css'); break; } } diff --git a/themes/bootstrap5/templates/RecordDriver/EDS/core.phtml b/themes/bootstrap5/templates/RecordDriver/EDS/core.phtml index efaec059007..4150df648eb 100644 --- a/themes/bootstrap5/templates/RecordDriver/EDS/core.phtml +++ b/themes/bootstrap5/templates/RecordDriver/EDS/core.phtml @@ -1,4 +1,4 @@ -headLink()->appendStylesheet('EDS.css'); ?> +assetPipeline()->appendStylesheet('EDS.css'); ?> driver->getItems('core'); $dbLabel = $this->driver->getDbLabel(); diff --git a/themes/bootstrap5/templates/RecordDriver/EDS/result-list.phtml b/themes/bootstrap5/templates/RecordDriver/EDS/result-list.phtml index 034be0e42bd..e168526999b 100644 --- a/themes/bootstrap5/templates/RecordDriver/EDS/result-list.phtml +++ b/themes/bootstrap5/templates/RecordDriver/EDS/result-list.phtml @@ -1,5 +1,5 @@ headLink()->appendStylesheet('EDS.css'); + $this->assetPipeline()->appendStylesheet('EDS.css'); $accessLevel = $this->driver->getAccessLevel(); $restrictedView = empty($accessLevel) ? false : true; $recordLinker = $this->recordLinker($this->results); diff --git a/themes/bootstrap5/templates/RecordDriver/EPF/core.phtml b/themes/bootstrap5/templates/RecordDriver/EPF/core.phtml index 751296290f5..378aa7e6183 100644 --- a/themes/bootstrap5/templates/RecordDriver/EPF/core.phtml +++ b/themes/bootstrap5/templates/RecordDriver/EPF/core.phtml @@ -1,4 +1,4 @@ -headLink()->appendStylesheet('EDS.css'); ?> +assetPipeline()->appendStylesheet('EDS.css'); ?> driver->getItems('core'); $accessLevel = $this->driver->getAccessLevel(); diff --git a/themes/bootstrap5/templates/RecordTab/map.phtml b/themes/bootstrap5/templates/RecordTab/map.phtml index afb163447a2..9317acec5ed 100644 --- a/themes/bootstrap5/templates/RecordTab/map.phtml +++ b/themes/bootstrap5/templates/RecordTab/map.phtml @@ -2,7 +2,7 @@ $this->headScript()->appendFile('vendor/leaflet/leaflet.js'); $this->headScript()->appendFile('vendor/leaflet/leaflet.latlng-graticule.js'); $this->headScript()->appendFile('map_tab_leaflet.js'); - $this->headLink()->appendStylesheet('vendor/leaflet/leaflet.css'); + $this->assetPipeline()->appendStylesheet('vendor/leaflet/leaflet.css'); $this->jsTranslations()->addStrings( ['Coordinates' => 'Coordinates', 'no_description' => 'no_description'] ); diff --git a/themes/bootstrap5/templates/channels/channelList.phtml b/themes/bootstrap5/templates/channels/channelList.phtml index 2812b28823b..1d056a5fc9f 100644 --- a/themes/bootstrap5/templates/channels/channelList.phtml +++ b/themes/bootstrap5/templates/channels/channelList.phtml @@ -1,6 +1,6 @@ headLink()->appendStylesheet('vendor/slick.css'); - $this->headLink()->appendStylesheet('vendor/slick-theme.css'); + $this->assetPipeline()->appendStylesheet('vendor/slick.css'); + $this->assetPipeline()->appendStylesheet('vendor/slick-theme.css'); $this->headScript()->appendFile('vendor/slick.min.js'); $this->headScript()->appendFile('channels.js'); $this->jsTranslations()->addStrings([ diff --git a/themes/bootstrap5/templates/combined/results.phtml b/themes/bootstrap5/templates/combined/results.phtml index 8dd2b6c1b9c..2c9ff3b4039 100644 --- a/themes/bootstrap5/templates/combined/results.phtml +++ b/themes/bootstrap5/templates/combined/results.phtml @@ -41,7 +41,7 @@ $this->render('search/results-scripts.phtml', compact('displayVersions')); $this->headScript()->appendFile('combined-search.js'); // Style - $this->headLink()->appendStylesheet('combined-search.css'); + $this->assetPipeline()->appendStylesheet('combined-search.css'); ?> flashmessages()?>

escapeHtml($headTitle)?>

diff --git a/themes/bootstrap5/templates/layout/layout.phtml b/themes/bootstrap5/templates/layout/layout.phtml index feb9cb0afb4..4f9dc009c5a 100644 --- a/themes/bootstrap5/templates/layout/layout.phtml +++ b/themes/bootstrap5/templates/layout/layout.phtml @@ -52,9 +52,9 @@ ?> layout()->rtl) { // RTL styling - $this->headLink()->appendStylesheet('vendor/bootstrap-rtl.min.css'); + $this->assetPipeline()->appendStylesheet('vendor/bootstrap-rtl.min.css'); } ?> - headLink()?> + assetPipeline()->outputAssets()?> headStyle()?> headScript()->appendFile('vendor/bootstrap-slider.min.js'); - $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css'); + $this->assetPipeline()->appendStylesheet('vendor/bootstrap-slider.min.css'); $min = !empty($current['values'][0]) ? min($current['values'][0], 1400) : 1400; $future = date('Y', time() + 31536000); $max = !empty($current['values'][1]) ? max($future, $current['values'][1]) : $future; diff --git a/themes/bootstrap5/templates/search/advanced/ranges.phtml b/themes/bootstrap5/templates/search/advanced/ranges.phtml index 00771cad49a..0d98d030f07 100644 --- a/themes/bootstrap5/templates/search/advanced/ranges.phtml +++ b/themes/bootstrap5/templates/search/advanced/ranges.phtml @@ -21,7 +21,7 @@ headScript()->appendFile('vendor/bootstrap-slider.min.js'); - $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css'); + $this->assetPipeline()->appendStylesheet('vendor/bootstrap-slider.min.css'); $min = !empty($current['values'][0]) ? min($current['values'][0], 1400) : 1400; $future = date('Y', time() + 31536000); $max = !empty($current['values'][1]) ? max($future, $current['values'][1]) : $future; diff --git a/themes/bootstrap5/templates/search/searchbox.phtml b/themes/bootstrap5/templates/search/searchbox.phtml index 4f2278a2aba..157c8ddd669 100644 --- a/themes/bootstrap5/templates/search/searchbox.phtml +++ b/themes/bootstrap5/templates/search/searchbox.phtml @@ -113,7 +113,7 @@ $this->headScript()->appendFile('vendor/js.cookie.js'); $this->headScript()->appendFile('vendor/simple-keyboard/index.js'); $this->headScript()->appendFile('vendor/simple-keyboard-layouts/index.js'); - $this->headLink()->appendStylesheet('vendor/simple-keyboard/index.css'); + $this->assetPipeline()->appendStylesheet('vendor/simple-keyboard/index.css'); ?> headScript()->appendFile('vendor/bootstrap-slider.min.js'); + $this->assetPipeline()->appendScriptFile('vendor/bootstrap-slider.min.js'); $this->assetPipeline()->appendStylesheet('vendor/bootstrap-slider.min.css'); $min = !empty($current['values'][0]) ? min($current['values'][0], 1400) : 1400; $future = date('Y', time() + 31536000); diff --git a/themes/bootstrap5/templates/search/facet-list.phtml b/themes/bootstrap5/templates/search/facet-list.phtml index 2f4f79290d5..49ae012c4fa 100644 --- a/themes/bootstrap5/templates/search/facet-list.phtml +++ b/themes/bootstrap5/templates/search/facet-list.phtml @@ -20,8 +20,8 @@ } $this->headTitle($this->translate('facet_list_for', ['%%field%%' => $this->facetLabel])); $multiFacetsSelection = $this->multiFacetsSelection ? 'true' : 'false'; - $this->headScript()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); - $this->headScript()->appendFile('facets.js'); + $this->assetPipeline()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); + $this->assetPipeline()->appendScriptFile('facets.js'); ?>

transEsc($this->facetLabel) ?>

diff --git a/themes/bootstrap5/templates/search/newitem.phtml b/themes/bootstrap5/templates/search/newitem.phtml index 1db52d3e79a..86ef332a432 100644 --- a/themes/bootstrap5/templates/search/newitem.phtml +++ b/themes/bootstrap5/templates/search/newitem.phtml @@ -6,7 +6,7 @@ $this->layout()->breadcrumbs = '
  • ' . $this->transEsc('New Items') . '
  • '; // Load advanced search Javascript to activate the clear button: - $this->headScript()->appendFile('advanced_search.js'); + $this->assetPipeline()->appendScriptFile('advanced_search.js'); // Convenience variable: $offlineMode = $this->ils()->getOfflineMode(); diff --git a/themes/bootstrap5/templates/search/results-scripts.phtml b/themes/bootstrap5/templates/search/results-scripts.phtml index a18328cd41e..89b8b54321d 100644 --- a/themes/bootstrap5/templates/search/results-scripts.phtml +++ b/themes/bootstrap5/templates/search/results-scripts.phtml @@ -1,16 +1,16 @@ headScript()->appendFile('check_item_statuses.js'); -$this->headScript()->appendFile('check_save_statuses.js'); +$this->assetPipeline()->appendScriptFile('check_item_statuses.js'); +$this->assetPipeline()->appendScriptFile('check_save_statuses.js'); if ($this->displayVersions) { - $this->headScript()->appendFile('record_versions.js'); - $this->headScript()->appendFile('combined-search.js'); + $this->assetPipeline()->appendScriptFile('record_versions.js'); + $this->assetPipeline()->appendScriptFile('combined-search.js'); } // Load only if list view parameter is NOT full: if (($this->listViewOption ?? 'full') !== 'full') { - $this->headScript()->appendFile('record.js'); - $this->headScript()->appendFile('embedded_record.js'); + $this->assetPipeline()->appendScriptFile('record.js'); + $this->assetPipeline()->appendScriptFile('embedded_record.js'); } if ($this->jsResults ?? false) { - $this->headScript()->appendFile('search.js'); + $this->assetPipeline()->appendScriptFile('search.js'); } diff --git a/themes/bootstrap5/templates/search/results.phtml b/themes/bootstrap5/templates/search/results.phtml index 0de19b881ed..9e70bc844e7 100644 --- a/themes/bootstrap5/templates/search/results.phtml +++ b/themes/bootstrap5/templates/search/results.phtml @@ -48,7 +48,7 @@ ); $recommendations = $this->results->getRecommendations('side'); $multiFacetsSelection = $this->multiFacetsSelection ? 'true' : 'false'; - $this->headScript()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); + $this->assetPipeline()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); ?>

    escapeHtml($headTitle)?>

    diff --git a/themes/bootstrap5/templates/search/searchTabs.phtml b/themes/bootstrap5/templates/search/searchTabs.phtml index 67e169396da..d30fd74c327 100644 --- a/themes/bootstrap5/templates/search/searchTabs.phtml +++ b/themes/bootstrap5/templates/search/searchTabs.phtml @@ -35,6 +35,6 @@ showCounts): ?> - headScript()->appendFile('resultcount.js'); ?> + assetPipeline()->appendScriptFile('resultcount.js'); ?> diff --git a/themes/bootstrap5/templates/search/searchbox.phtml b/themes/bootstrap5/templates/search/searchbox.phtml index 157c8ddd669..e3f17888b9f 100644 --- a/themes/bootstrap5/templates/search/searchbox.phtml +++ b/themes/bootstrap5/templates/search/searchbox.phtml @@ -110,9 +110,9 @@ headScript()->appendFile('vendor/js.cookie.js'); - $this->headScript()->appendFile('vendor/simple-keyboard/index.js'); - $this->headScript()->appendFile('vendor/simple-keyboard-layouts/index.js'); + $this->assetPipeline()->appendScriptFile('vendor/js.cookie.js'); + $this->assetPipeline()->appendScriptFile('vendor/simple-keyboard/index.js'); + $this->assetPipeline()->appendScriptFile('vendor/simple-keyboard-layouts/index.js'); $this->assetPipeline()->appendStylesheet('vendor/simple-keyboard/index.css'); ?> assetPipeline()->appendScriptFile('vendor/bootstrap-slider.min.js'); - $this->assetPipeline()->appendStylesheet('vendor/bootstrap-slider.min.css'); + $this->assetManager()->appendScriptFile('vendor/bootstrap-slider.min.js'); + $this->assetManager()->appendStylesheet('vendor/bootstrap-slider.min.css'); $min = !empty($current['values'][0]) ? min($current['values'][0], 1400) : 1400; $future = date('Y', time() + 31536000); $max = !empty($current['values'][1]) ? max($future, $current['values'][1]) : $future; diff --git a/themes/bootstrap5/templates/search/facet-list.phtml b/themes/bootstrap5/templates/search/facet-list.phtml index 49ae012c4fa..8adaff9f7fe 100644 --- a/themes/bootstrap5/templates/search/facet-list.phtml +++ b/themes/bootstrap5/templates/search/facet-list.phtml @@ -20,8 +20,8 @@ } $this->headTitle($this->translate('facet_list_for', ['%%field%%' => $this->facetLabel])); $multiFacetsSelection = $this->multiFacetsSelection ? 'true' : 'false'; - $this->assetPipeline()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); - $this->assetPipeline()->appendScriptFile('facets.js'); + $this->assetManager()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); + $this->assetManager()->appendScriptFile('facets.js'); ?>

    transEsc($this->facetLabel) ?>

    diff --git a/themes/bootstrap5/templates/search/newitem.phtml b/themes/bootstrap5/templates/search/newitem.phtml index 86ef332a432..ffd02d26e84 100644 --- a/themes/bootstrap5/templates/search/newitem.phtml +++ b/themes/bootstrap5/templates/search/newitem.phtml @@ -6,7 +6,7 @@ $this->layout()->breadcrumbs = '
  • ' . $this->transEsc('New Items') . '
  • '; // Load advanced search Javascript to activate the clear button: - $this->assetPipeline()->appendScriptFile('advanced_search.js'); + $this->assetManager()->appendScriptFile('advanced_search.js'); // Convenience variable: $offlineMode = $this->ils()->getOfflineMode(); diff --git a/themes/bootstrap5/templates/search/results-scripts.phtml b/themes/bootstrap5/templates/search/results-scripts.phtml index 89b8b54321d..798b679bffc 100644 --- a/themes/bootstrap5/templates/search/results-scripts.phtml +++ b/themes/bootstrap5/templates/search/results-scripts.phtml @@ -1,16 +1,16 @@ assetPipeline()->appendScriptFile('check_item_statuses.js'); -$this->assetPipeline()->appendScriptFile('check_save_statuses.js'); +$this->assetManager()->appendScriptFile('check_item_statuses.js'); +$this->assetManager()->appendScriptFile('check_save_statuses.js'); if ($this->displayVersions) { - $this->assetPipeline()->appendScriptFile('record_versions.js'); - $this->assetPipeline()->appendScriptFile('combined-search.js'); + $this->assetManager()->appendScriptFile('record_versions.js'); + $this->assetManager()->appendScriptFile('combined-search.js'); } // Load only if list view parameter is NOT full: if (($this->listViewOption ?? 'full') !== 'full') { - $this->assetPipeline()->appendScriptFile('record.js'); - $this->assetPipeline()->appendScriptFile('embedded_record.js'); + $this->assetManager()->appendScriptFile('record.js'); + $this->assetManager()->appendScriptFile('embedded_record.js'); } if ($this->jsResults ?? false) { - $this->assetPipeline()->appendScriptFile('search.js'); + $this->assetManager()->appendScriptFile('search.js'); } diff --git a/themes/bootstrap5/templates/search/results.phtml b/themes/bootstrap5/templates/search/results.phtml index 9d8eb103a35..6961c6a1f0a 100644 --- a/themes/bootstrap5/templates/search/results.phtml +++ b/themes/bootstrap5/templates/search/results.phtml @@ -48,7 +48,7 @@ ); $recommendations = $this->results->getRecommendations('side'); $multiFacetsSelection = $this->multiFacetsSelection ? 'true' : 'false'; - $this->assetPipeline()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); + $this->assetManager()->appendScript('var multiFacetsSelectionEnabled = ' . $multiFacetsSelection . ';'); ?>

    escapeHtml($headTitle)?>

    diff --git a/themes/bootstrap5/templates/search/searchTabs.phtml b/themes/bootstrap5/templates/search/searchTabs.phtml index d30fd74c327..0728104b43d 100644 --- a/themes/bootstrap5/templates/search/searchTabs.phtml +++ b/themes/bootstrap5/templates/search/searchTabs.phtml @@ -35,6 +35,6 @@ showCounts): ?> - assetPipeline()->appendScriptFile('resultcount.js'); ?> + assetManager()->appendScriptFile('resultcount.js'); ?> diff --git a/themes/bootstrap5/templates/search/searchbox.phtml b/themes/bootstrap5/templates/search/searchbox.phtml index e3f17888b9f..c0604086cdb 100644 --- a/themes/bootstrap5/templates/search/searchbox.phtml +++ b/themes/bootstrap5/templates/search/searchbox.phtml @@ -110,10 +110,10 @@ assetPipeline()->appendScriptFile('vendor/js.cookie.js'); - $this->assetPipeline()->appendScriptFile('vendor/simple-keyboard/index.js'); - $this->assetPipeline()->appendScriptFile('vendor/simple-keyboard-layouts/index.js'); - $this->assetPipeline()->appendStylesheet('vendor/simple-keyboard/index.css'); + $this->assetManager()->appendScriptFile('vendor/js.cookie.js'); + $this->assetManager()->appendScriptFile('vendor/simple-keyboard/index.js'); + $this->assetManager()->appendScriptFile('vendor/simple-keyboard-layouts/index.js'); + $this->assetManager()->appendStylesheet('vendor/simple-keyboard/index.css'); ?> recommend->isFullMatch() && $this->recommend->redirectFullMatch()): ?> escapeJs($url) . '";'; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $redirect, 'SET')?> + assetManager()->outputInlineScript($redirect)?> diff --git a/themes/bootstrap5/templates/Recommend/MapSelection.phtml b/themes/bootstrap5/templates/Recommend/MapSelection.phtml index d96a09c11dc..b9dd89845c3 100644 --- a/themes/bootstrap5/templates/Recommend/MapSelection.phtml +++ b/themes/bootstrap5/templates/Recommend/MapSelection.phtml @@ -41,7 +41,7 @@  transEsc('link_text_need_help')?>
    - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $jsLoad, 'SET')?> + assetManager()->outputInlineScript($jsLoad)?> transEsc('draw_searchbox_start') . '";' . 'L.drawLocal.draw.handlers.simpleshape.tooltip.end = "' . $this->transEsc('draw_searchbox_end') . '";' ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $loadTranslations, 'SET')?> + assetManager()->outputInlineScript($loadTranslations)?> diff --git a/themes/bootstrap5/templates/Recommend/PubDateVisAjax.phtml b/themes/bootstrap5/templates/Recommend/PubDateVisAjax.phtml index 9cf630bea99..0640fd4ce7c 100644 --- a/themes/bootstrap5/templates/Recommend/PubDateVisAjax.phtml +++ b/themes/bootstrap5/templates/Recommend/PubDateVisAjax.phtml @@ -21,7 +21,7 @@ $js = "loadVis('" . $this->recommend->getFacetFields() . "', '" . $this->recommend->getSearchParams() . "', VuFind.path, " . $this->recommend->getZooming() . ');'; - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $js, 'SET'); + echo $this->assetManager()->outputInlineScript($js); ?> diff --git a/themes/bootstrap5/templates/Recommend/SideFacets.phtml b/themes/bootstrap5/templates/Recommend/SideFacets.phtml index d2d1c689ae7..df0c403dd16 100644 --- a/themes/bootstrap5/templates/Recommend/SideFacets.phtml +++ b/themes/bootstrap5/templates/Recommend/SideFacets.phtml @@ -73,4 +73,4 @@ -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, 'registerSideFacetTruncation();', 'SET');?> +assetManager()->outputInlineScript('registerSideFacetTruncation();');?> diff --git a/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml b/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml index 2214d7a6702..ead190f0e90 100644 --- a/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml +++ b/themes/bootstrap5/templates/Recommend/SideFacets/range-slider.phtml @@ -82,5 +82,5 @@ }); JS; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> + assetManager()->outputInlineScript($script); ?> diff --git a/themes/bootstrap5/templates/Recommend/TopFacets.phtml b/themes/bootstrap5/templates/Recommend/TopFacets.phtml index 3a27476f62c..3d417f4c5ee 100644 --- a/themes/bootstrap5/templates/Recommend/TopFacets.phtml +++ b/themes/bootstrap5/templates/Recommend/TopFacets.phtml @@ -58,4 +58,4 @@ VuFind.truncate.initTruncate('.top-facets-contents'); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET');?> +assetManager()->outputInlineScript($script);?> diff --git a/themes/bootstrap5/templates/Recommend/VisualFacets.phtml b/themes/bootstrap5/templates/Recommend/VisualFacets.phtml index 371f7c625b2..b66db03f187 100644 --- a/themes/bootstrap5/templates/Recommend/VisualFacets.phtml +++ b/themes/bootstrap5/templates/Recommend/VisualFacets.phtml @@ -52,5 +52,5 @@ }); JS; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET');?> + assetManager()->outputInlineScript($script);?> diff --git a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/list-entry.phtml b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/list-entry.phtml index e34ce8929b3..d7507f5c88e 100644 --- a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/list-entry.phtml +++ b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/list-entry.phtml @@ -256,5 +256,5 @@ }); JS; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET');?> + assetManager()->outputInlineScript($script);?> diff --git a/themes/bootstrap5/templates/RecordTab/hierarchytree.phtml b/themes/bootstrap5/templates/RecordTab/hierarchytree.phtml index 6010e3eb8b1..b7e5dfb3b87 100644 --- a/themes/bootstrap5/templates/RecordTab/hierarchytree.phtml +++ b/themes/bootstrap5/templates/RecordTab/hierarchytree.phtml @@ -61,9 +61,9 @@ inlineScript(\Laminas\View\Helper\HeadScript::FILE, 'hierarchy_tree.js'); + echo $this->assetManager()->outputInlineScriptFile('hierarchy_tree.js'); $js = << VuFind.hierarchyTree.initTree(el)); JS; - echo $this->inlineScript()->appendScript($js); + echo $this->assetManager()->outputInlineScript($js); ?> diff --git a/themes/bootstrap5/templates/RecordTab/map.phtml b/themes/bootstrap5/templates/RecordTab/map.phtml index a899e09e555..f5b4e93b123 100644 --- a/themes/bootstrap5/templates/RecordTab/map.phtml +++ b/themes/bootstrap5/templates/RecordTab/map.phtml @@ -17,5 +17,5 @@ ?>
    - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $jsLoad, 'SET')?> + assetManager()->outputInlineScript($jsLoad)?>
    diff --git a/themes/bootstrap5/templates/RecordTab/similaritemscarousel.phtml b/themes/bootstrap5/templates/RecordTab/similaritemscarousel.phtml index 951de2e3f94..ddcf99a218a 100644 --- a/themes/bootstrap5/templates/RecordTab/similaritemscarousel.phtml +++ b/themes/bootstrap5/templates/RecordTab/similaritemscarousel.phtml @@ -75,4 +75,4 @@ $('#similar-items-carousel img').on('load', normalizeHeights); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/RecordTab/versions.phtml b/themes/bootstrap5/templates/RecordTab/versions.phtml index a3362e46afb..1af3efe6218 100644 --- a/themes/bootstrap5/templates/RecordTab/versions.phtml +++ b/themes/bootstrap5/templates/RecordTab/versions.phtml @@ -55,4 +55,4 @@ $script = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> +assetManager()->outputInlineScript($script); ?> diff --git a/themes/bootstrap5/templates/admin/feedback/home.phtml b/themes/bootstrap5/templates/admin/feedback/home.phtml index dcb80593fd6..1ef16c0f80f 100644 --- a/themes/bootstrap5/templates/admin/feedback/home.phtml +++ b/themes/bootstrap5/templates/admin/feedback/home.phtml @@ -176,4 +176,4 @@ $js = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $js, 'SET');?> +assetManager()->outputInlineScript($js);?> diff --git a/themes/bootstrap5/templates/cart/cart.phtml b/themes/bootstrap5/templates/cart/cart.phtml index e085ecb4c20..3d026cba08f 100644 --- a/themes/bootstrap5/templates/cart/cart.phtml +++ b/themes/bootstrap5/templates/cart/cart.phtml @@ -121,4 +121,4 @@ }); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/cart/email.phtml b/themes/bootstrap5/templates/cart/email.phtml index a5822b53e15..475cb8d5d38 100644 --- a/themes/bootstrap5/templates/cart/email.phtml +++ b/themes/bootstrap5/templates/cart/email.phtml @@ -40,4 +40,4 @@ $('#itemhide').removeClass('in'); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/cart/export.phtml b/themes/bootstrap5/templates/cart/export.phtml index 321172b36f3..25a95c5a372 100644 --- a/themes/bootstrap5/templates/cart/export.phtml +++ b/themes/bootstrap5/templates/cart/export.phtml @@ -72,4 +72,4 @@ }).trigger('change'); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/cart/save.phtml b/themes/bootstrap5/templates/cart/save.phtml index cb4b2ca1872..6f6ba9c2737 100644 --- a/themes/bootstrap5/templates/cart/save.phtml +++ b/themes/bootstrap5/templates/cart/save.phtml @@ -66,4 +66,4 @@ $('#itemhide').removeClass('in'); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/collection/view.phtml b/themes/bootstrap5/templates/collection/view.phtml index 46797fe338d..74a2e81f891 100644 --- a/themes/bootstrap5/templates/collection/view.phtml +++ b/themes/bootstrap5/templates/collection/view.phtml @@ -98,4 +98,4 @@ driver->supportsCoinsOpenURL() ? '' : ''?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, '$(document).ready(recordDocReady);', 'SET'); ?> +assetManager()->outputInlineScript('$(document).ready(recordDocReady);'); ?> diff --git a/themes/bootstrap5/templates/combined/results-ajax.phtml b/themes/bootstrap5/templates/combined/results-ajax.phtml index 2782549f91b..f59177d147d 100644 --- a/themes/bootstrap5/templates/combined/results-ajax.phtml +++ b/themes/bootstrap5/templates/combined/results-ajax.phtml @@ -30,5 +30,5 @@

    icon('spinner') ?> transEsc('loading_ellipsis')?>

    -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $loadJs, 'SET')?> +assetManager()->outputInlineScript($loadJs)?> diff --git a/themes/bootstrap5/templates/combined/results-list.phtml b/themes/bootstrap5/templates/combined/results-list.phtml index 5178966bf7a..16274f04a63 100644 --- a/themes/bootstrap5/templates/combined/results-list.phtml +++ b/themes/bootstrap5/templates/combined/results-list.phtml @@ -75,7 +75,7 @@ } JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $countsJs, 'SET')?> +assetManager()->outputInlineScript($countsJs)?>
    @@ -110,7 +110,7 @@ $recommendationClass = str_replace('\\', '_', $current::class); $recommendationJs = $generateMoveRecommendationToSidebarJavascript($recommendationClass); ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $recommendationJs, 'SET')?> + assetManager()->outputInlineScript($recommendationJs)?> @@ -156,7 +156,7 @@ $recommendationClass = str_replace('\\', '_', $current::class); $recommendationJs = $generateMoveRecommendationToSidebarJavascript($recommendationClass); ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $recommendationJs, 'SET')?> + assetManager()->outputInlineScript($recommendationJs)?> diff --git a/themes/bootstrap5/templates/devtools/language.phtml b/themes/bootstrap5/templates/devtools/language.phtml index 38486a93b12..14e7fe25ad5 100644 --- a/themes/bootstrap5/templates/devtools/language.phtml +++ b/themes/bootstrap5/templates/devtools/language.phtml @@ -175,4 +175,4 @@ Current filter mode: -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/holds/edit.phtml b/themes/bootstrap5/templates/holds/edit.phtml index 230a77afca5..a7ef7fab0ed 100644 --- a/themes/bootstrap5/templates/holds/edit.phtml +++ b/themes/bootstrap5/templates/holds/edit.phtml @@ -86,7 +86,7 @@ inlineScript()->appendFile('hold.js'); + echo $this->assetManager()->outputInlineScriptFile('hold.js'); $js = <<inlineScript()->appendScript($js); + echo $this->assetManager()->outputInlineScript($js); ?> diff --git a/themes/bootstrap5/templates/install/showsql.phtml b/themes/bootstrap5/templates/install/showsql.phtml index 841e1f74172..88388db3fea 100644 --- a/themes/bootstrap5/templates/install/showsql.phtml +++ b/themes/bootstrap5/templates/install/showsql.phtml @@ -29,4 +29,4 @@ $script = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +assetManager()->outputInlineScript($script); diff --git a/themes/bootstrap5/templates/layout/layout.phtml b/themes/bootstrap5/templates/layout/layout.phtml index d75e2899ef9..7d11c6b8263 100644 --- a/themes/bootstrap5/templates/layout/layout.phtml +++ b/themes/bootstrap5/templates/layout/layout.phtml @@ -174,7 +174,7 @@
    render('Helpers/analytics.phtml')?> captcha()->js() as $jsInclude):?> - inlineScript(\Laminas\View\Helper\HeadScript::FILE, $jsInclude, 'SET')?> + assetManager()->outputInlineScriptFile($jsInclude)?> assetManager()->outputFooterAssets() ?> diff --git a/themes/bootstrap5/templates/layout/lightbox.phtml b/themes/bootstrap5/templates/layout/lightbox.phtml index 6649acb9814..6829a261e9b 100644 --- a/themes/bootstrap5/templates/layout/lightbox.phtml +++ b/themes/bootstrap5/templates/layout/lightbox.phtml @@ -3,6 +3,9 @@ matomo(['context' => $this->layoutContext ?? 'lightbox'])?> googleanalytics($this->serverUrl(true))?> session()->put('reset_account_status', null) && $this->auth()->getManager()->ajaxEnabled()) { - $this->inlineScript()->setAllowArbitraryAttributes(true); - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, 'VuFind.account.clearAllCaches();', 'SET', ['data-lightbox-run' => 'always']); + echo $this->assetManager()->outputInlineScript( + script: 'VuFind.account.clearAllCaches();', + attrs: ['data-lightbox-run' => 'always'], + allowArbitraryAttrs: true + ); } diff --git a/themes/bootstrap5/templates/librarycards/editcard.phtml b/themes/bootstrap5/templates/librarycards/editcard.phtml index 89a0ab96c66..6afe542bc6c 100644 --- a/themes/bootstrap5/templates/librarycards/editcard.phtml +++ b/themes/bootstrap5/templates/librarycards/editcard.phtml @@ -55,6 +55,6 @@ $script = "setupMultiILSLoginFields($methods, 'login_');"; // Inline the script for lightbox compatibility - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); + echo $this->assetManager()->outputInlineScript($script); } ?> diff --git a/themes/bootstrap5/templates/myresearch/cataloglogin.phtml b/themes/bootstrap5/templates/myresearch/cataloglogin.phtml index 87a03c586a8..4ec5cd59288 100644 --- a/themes/bootstrap5/templates/myresearch/cataloglogin.phtml +++ b/themes/bootstrap5/templates/myresearch/cataloglogin.phtml @@ -69,7 +69,7 @@ $script = "setupMultiILSLoginFields($methods, 'profile_cat_');"; // Inline the script for lightbox compatibility - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); + echo $this->assetManager()->outputInlineScript($script); } ?> diff --git a/themes/bootstrap5/templates/myresearch/deleteaccount.phtml b/themes/bootstrap5/templates/myresearch/deleteaccount.phtml index b8ee8b5ebab..4eb862d1cd1 100644 --- a/themes/bootstrap5/templates/myresearch/deleteaccount.phtml +++ b/themes/bootstrap5/templates/myresearch/deleteaccount.phtml @@ -7,7 +7,7 @@ // Logout redirect with inline script to make it lightbox compatible $script = "setTimeout(function() { window.location = '{$this->redirectUrl}'; }, 3000);"; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> + assetManager()->outputInlineScript($script); ?>
    diff --git a/themes/bootstrap5/templates/myresearch/notify-account-status.phtml b/themes/bootstrap5/templates/myresearch/notify-account-status.phtml index ff256d68886..5bc4cc431c3 100644 --- a/themes/bootstrap5/templates/myresearch/notify-account-status.phtml +++ b/themes/bootstrap5/templates/myresearch/notify-account-status.phtml @@ -1,5 +1,5 @@ accountStatus) { $notifyScript = 'VuFind.account.notify("' . $this->method . '", ' . json_encode($this->accountStatus) . ');'; - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $notifyScript, 'SET'); + echo $this->assetManager()->outputInlineScript($notifyScript); } diff --git a/themes/bootstrap5/templates/record/hold.phtml b/themes/bootstrap5/templates/record/hold.phtml index 294053a6e7a..59dbb86cb7b 100644 --- a/themes/bootstrap5/templates/record/hold.phtml +++ b/themes/bootstrap5/templates/record/hold.phtml @@ -143,7 +143,7 @@ inlineScript()->appendFile('hold.js'); + echo $this->assetManager()->outputInlineScriptFile('hold.js'); $js = <<inlineScript()->appendScript($js); + echo $this->assetManager()->outputInlineScript($js); ?> diff --git a/themes/bootstrap5/templates/record/illrequest.phtml b/themes/bootstrap5/templates/record/illrequest.phtml index 9fb855d9832..74c52144d50 100644 --- a/themes/bootstrap5/templates/record/illrequest.phtml +++ b/themes/bootstrap5/templates/record/illrequest.phtml @@ -112,7 +112,7 @@ inlineScript()->appendFile('ill.js'); + echo $this->assetManager()->outputInlineScriptFile('ill.js'); $js = <<inlineScript()->appendScript($js); + echo $this->assetManager()->outputInlineScript($js); ?> diff --git a/themes/bootstrap5/templates/record/sms.phtml b/themes/bootstrap5/templates/record/sms.phtml index 2774dd102a7..d92b3352ca0 100644 --- a/themes/bootstrap5/templates/record/sms.phtml +++ b/themes/bootstrap5/templates/record/sms.phtml @@ -1,7 +1,7 @@ headTitle($this->translate('Text this')); - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::FILE, 'vendor/libphonenumber.js', 'SET'); + echo $this->assetManager()->outputInlineScriptFile('vendor/libphonenumber.js'); // Set up breadcrumbs: $this->layout()->breadcrumbs = $this->searchMemory()->getLastSearchLink($this->transEsc('Search'), '
  • ', '
  • ') diff --git a/themes/bootstrap5/templates/record/storageretrievalrequest.phtml b/themes/bootstrap5/templates/record/storageretrievalrequest.phtml index 0bcc9ceb644..895cf26ee59 100644 --- a/themes/bootstrap5/templates/record/storageretrievalrequest.phtml +++ b/themes/bootstrap5/templates/record/storageretrievalrequest.phtml @@ -113,4 +113,4 @@ }); JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $changeHandler, 'SET'); +assetManager()->outputInlineScript($changeHandler); diff --git a/themes/bootstrap5/templates/record/taglist.phtml b/themes/bootstrap5/templates/record/taglist.phtml index 184374ce042..0d2eb264b19 100644 --- a/themes/bootstrap5/templates/record/taglist.phtml +++ b/themes/bootstrap5/templates/record/taglist.phtml @@ -32,4 +32,4 @@ $script = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +assetManager()->outputInlineScript($script); diff --git a/themes/bootstrap5/templates/record/view.phtml b/themes/bootstrap5/templates/record/view.phtml index 653480ec0db..80085a60ef6 100644 --- a/themes/bootstrap5/templates/record/view.phtml +++ b/themes/bootstrap5/templates/record/view.phtml @@ -98,4 +98,4 @@ -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, '$(document).ready(recordDocReady);', 'SET'); ?> +assetManager()->outputInlineScript('$(document).ready(recordDocReady);'); ?> diff --git a/themes/bootstrap5/templates/relais/button.phtml b/themes/bootstrap5/templates/relais/button.phtml index 8a9ca2197ea..fdee2d66a04 100644 --- a/themes/bootstrap5/templates/relais/button.phtml +++ b/themes/bootstrap5/templates/relais/button.phtml @@ -26,6 +26,6 @@ $oclc = $this->escapeJs($driver->tryMethod('getCleanOCLCNum')); $failLink = $this->escapeJs($this->relais()->getSearchLink($driver)); $activateRelais = "$(document).ready(function() { VuFind.relais.checkAvailability('$addLink', '$oclc', '$failLink') });"; - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $activateRelais, 'SET'); + echo $this->assetManager()->outputInlineScript($activateRelais); } ?> \ No newline at end of file diff --git a/themes/bootstrap5/templates/relais/request.phtml b/themes/bootstrap5/templates/relais/request.phtml index 29dc7e7c95d..fae50e7dfb1 100644 --- a/themes/bootstrap5/templates/relais/request.phtml +++ b/themes/bootstrap5/templates/relais/request.phtml @@ -10,5 +10,5 @@ assetManager()->appendScriptFile('relais.js'); $activateRelais = "$(document).ready(function () { VuFind.relais.addItem('$oclc', '$failLink'); });\n"; - echo $this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $activateRelais, 'SET') + echo $this->assetManager()->outputInlineScript($activateRelais); ?> diff --git a/themes/bootstrap5/templates/search/advanced/eds.phtml b/themes/bootstrap5/templates/search/advanced/eds.phtml index 02018121c3e..0e2644d055e 100644 --- a/themes/bootstrap5/templates/search/advanced/eds.phtml +++ b/themes/bootstrap5/templates/search/advanced/eds.phtml @@ -142,6 +142,6 @@ }); JS; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> + assetManager()->outputInlineScript($script); ?> diff --git a/themes/bootstrap5/templates/search/advanced/layout.phtml b/themes/bootstrap5/templates/search/advanced/layout.phtml index 5e83106b01a..5277076c849 100644 --- a/themes/bootstrap5/templates/search/advanced/layout.phtml +++ b/themes/bootstrap5/templates/search/advanced/layout.phtml @@ -235,4 +235,4 @@ $script = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +assetManager()->outputInlineScript($script); diff --git a/themes/bootstrap5/templates/search/advanced/ranges.phtml b/themes/bootstrap5/templates/search/advanced/ranges.phtml index affb413fcdf..aff11c74d97 100644 --- a/themes/bootstrap5/templates/search/advanced/ranges.phtml +++ b/themes/bootstrap5/templates/search/advanced/ranges.phtml @@ -70,7 +70,7 @@ }); JS; ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> + assetManager()->outputInlineScript($script); ?> diff --git a/themes/bootstrap5/templates/search/facet-list.phtml b/themes/bootstrap5/templates/search/facet-list.phtml index 8adaff9f7fe..9c064baca82 100644 --- a/themes/bootstrap5/templates/search/facet-list.phtml +++ b/themes/bootstrap5/templates/search/facet-list.phtml @@ -67,7 +67,7 @@ -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, 'VuFind.facetList.setup();', 'SET')?> +assetManager()->outputInlineScript('VuFind.facetList.setup();')?> inLightbox): ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, 'VuFind.lightbox_facets.setup();', 'SET')?> + assetManager()->outputInlineScript('VuFind.lightbox_facets.setup();')?> diff --git a/themes/bootstrap5/templates/search/history.phtml b/themes/bootstrap5/templates/search/history.phtml index 993b7c43129..2fddaf20100 100644 --- a/themes/bootstrap5/templates/search/history.phtml +++ b/themes/bootstrap5/templates/search/history.phtml @@ -76,4 +76,4 @@ }) JS; ?> -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET') ?> +assetManager()->outputInlineScript($script) ?> diff --git a/themes/bootstrap5/templates/search/home.phtml b/themes/bootstrap5/templates/search/home.phtml index e25140bec8e..f296ba5069c 100644 --- a/themes/bootstrap5/templates/search/home.phtml +++ b/themes/bootstrap5/templates/search/home.phtml @@ -17,7 +17,7 @@
    slot('search-home-hero')->start() ?> render('search/searchbox.phtml')?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, '$("#searchForm_lookfor").focus();', 'SET'); ?> + assetManager()->outputInlineScript('$("#searchForm_lookfor").focus();'); ?> slot('search-home-hero')->end() ?>
    diff --git a/themes/bootstrap5/templates/search/reservessearch.phtml b/themes/bootstrap5/templates/search/reservessearch.phtml index a46f9b260aa..228e1126495 100644 --- a/themes/bootstrap5/templates/search/reservessearch.phtml +++ b/themes/bootstrap5/templates/search/reservessearch.phtml @@ -48,7 +48,7 @@ ] ); ?> - inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, "$('#reservesSearchForm_lookfor').focus()", 'SET')?> + assetManager()->outputInlineScript("$('#reservesSearchForm_lookfor').focus()")?>
    diff --git a/themes/bootstrap5/templates/turnstile/challenge.phtml b/themes/bootstrap5/templates/turnstile/challenge.phtml index 642ddeaf1d5..e92d75393af 100644 --- a/themes/bootstrap5/templates/turnstile/challenge.phtml +++ b/themes/bootstrap5/templates/turnstile/challenge.phtml @@ -7,7 +7,7 @@ } JS; ?> -inlineScript()->appendScript($js)?> +assetManager()->outputInlineScript($js)?>
    -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +assetManager()->outputInlineScript($script); diff --git a/themes/bootstrap5/templates/upgrade/fixduplicatetags.phtml b/themes/bootstrap5/templates/upgrade/fixduplicatetags.phtml index a58434f27b8..ddc1085da4d 100644 --- a/themes/bootstrap5/templates/upgrade/fixduplicatetags.phtml +++ b/themes/bootstrap5/templates/upgrade/fixduplicatetags.phtml @@ -31,4 +31,4 @@ $script = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +assetManager()->outputInlineScript($script); diff --git a/themes/bootstrap5/templates/upgrade/fixmetadata.phtml b/themes/bootstrap5/templates/upgrade/fixmetadata.phtml index a6f44efb7bb..cb57a7597a7 100644 --- a/themes/bootstrap5/templates/upgrade/fixmetadata.phtml +++ b/themes/bootstrap5/templates/upgrade/fixmetadata.phtml @@ -25,4 +25,4 @@ $script = << -inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +assetManager()->outputInlineScript($script); From 92f14b5a42d2e0c653b64e5a3afd441fd0786df8 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Mon, 3 Feb 2025 14:38:44 -0500 Subject: [PATCH 15/30] Fix test. --- .../VuFindTest/View/Helper/Root/RecordDataFormatterTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php index f2071e9c0b7..00740da1b94 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php @@ -528,8 +528,7 @@ public function testFormatting(string $function): void 'Multi Data' => 'Book', 'Subjects' => 'Naples (Kingdom) History Spanish rule, 1442-1707 Sources', 'Online Access' => 'http://fictional.com/sample/url', - // Double slash at the end comes from inline javascript - 'Tags' => 'Add Tag No Tags, Be the first to tag this record! //', + 'Tags' => 'Add Tag No Tags, Be the first to tag this record!', 'ZeroAllowed' => 0, 'c' => 'c', 'a' => 'a', From 7e3c158c975a086f376ab3cffd209a9b7306d347 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 06:51:38 -0500 Subject: [PATCH 16/30] Revert "Fix test." This reverts commit 92f14b5a42d2e0c653b64e5a3afd441fd0786df8. --- .../VuFindTest/View/Helper/Root/RecordDataFormatterTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php index 00740da1b94..f2071e9c0b7 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php @@ -528,7 +528,8 @@ public function testFormatting(string $function): void 'Multi Data' => 'Book', 'Subjects' => 'Naples (Kingdom) History Spanish rule, 1442-1707 Sources', 'Online Access' => 'http://fictional.com/sample/url', - 'Tags' => 'Add Tag No Tags, Be the first to tag this record!', + // Double slash at the end comes from inline javascript + 'Tags' => 'Add Tag No Tags, Be the first to tag this record! //', 'ZeroAllowed' => 0, 'c' => 'c', 'a' => 'a', From 4c6b3372ba38341a61ec2fbf133ca186202f4cfb Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 06:52:09 -0500 Subject: [PATCH 17/30] Update GoogleAnalytics helper to use AssetManager; improve tests. --- .../View/Helper/Root/GoogleAnalytics.php | 6 ++-- .../src/VuFindTest/Feature/ViewTrait.php | 35 +++++++++++++++---- .../VuFindTest/View/Helper/Root/IconTest.php | 2 -- .../Helper/Root/RecordDataFormatterTest.php | 1 - .../View/Helper/AssetManagerFactory.php | 2 +- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php b/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php index 4a0b1dc959e..dc31b1eb421 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php @@ -107,11 +107,11 @@ public function __invoke($customUrl = false) if (!$this->key) { return ''; } - $inlineScript = $this->getView()->plugin('inlinescript'); + $assetManager = $this->getView()->plugin('assetManager'); $url = 'https://www.googletagmanager.com/gtag/js?id=' . urlencode($this->key); $code = $this->getRawJavascript($customUrl); return - $inlineScript(HeadScript::FILE, $url, 'SET', ['async' => true]) . "\n" - . $inlineScript(HeadScript::SCRIPT, $code, 'SET'); + $assetManager->outputInlineScriptFile($url, attrs: ['async' => true]) . "\n" + . $assetManager->outputInlineScript($code); } } diff --git a/module/VuFind/src/VuFindTest/Feature/ViewTrait.php b/module/VuFind/src/VuFindTest/Feature/ViewTrait.php index 2358f13148c..1f52c71094e 100644 --- a/module/VuFind/src/VuFindTest/Feature/ViewTrait.php +++ b/module/VuFind/src/VuFindTest/Feature/ViewTrait.php @@ -29,7 +29,11 @@ namespace VuFindTest\Feature; +use Laminas\View\Renderer\PhpRenderer; use VuFind\View\Helper\Root\SearchMemory; +use VuFindTest\Container\MockContainer; +use VuFindTheme\View\Helper\AssetManager; +use VuFindTheme\View\Helper\AssetManagerFactory; /** * Trait for tests involving Laminas Views. @@ -42,13 +46,29 @@ */ trait ViewTrait { + /** + * Get a working AssetManager helper. + * + * @param PhpRenderer $renderer View for helper + * + * @return AssetManager + */ + protected function getAssetManager(PhpRenderer $renderer): AssetManager + { + $container = new MockContainer($this); + $factory = new AssetManagerFactory(); + $helper = $factory($container, AssetManager::class); + $helper->setView($renderer); + return $helper; + } + /** * Get a working renderer. * * @param array $plugins Custom VuFind plug-ins to register * @param string $theme Theme directory to load from * - * @return \Laminas\View\Renderer\PhpRenderer + * @return PhpRenderer */ protected function getPhpRenderer($plugins = [], $theme = 'bootstrap5') { @@ -63,13 +83,14 @@ protected function getPhpRenderer($plugins = [], $theme = 'bootstrap5') $this->getPathForTheme($theme), ] ); - $renderer = new \Laminas\View\Renderer\PhpRenderer(); + $renderer = new PhpRenderer(); $renderer->setResolver($resolver); - if (!empty($plugins)) { - $pluginManager = $renderer->getHelperPluginManager(); - foreach ($plugins as $key => $value) { - $pluginManager->setService($key, $value); - } + $pluginManager = $renderer->getHelperPluginManager(); + if (!isset($plugins['assetManager'])) { + $plugins['assetManager'] = $this->getAssetManager($renderer); + } + foreach ($plugins as $key => $value) { + $pluginManager->setService($key, $value); } return $renderer; } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/IconTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/IconTest.php index 89be1f39a60..c5593bfa2df 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/IconTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/IconTest.php @@ -34,7 +34,6 @@ use Laminas\View\Helper\EscapeHtmlAttr; use VuFind\Escaper\Escaper; use VuFind\View\Helper\Root\Icon; -use VuFindTheme\View\Helper\AssetManager; use VuFindTheme\View\Helper\ImageLink; /** @@ -138,7 +137,6 @@ protected function getIconHelper( ); $plugins = array_merge( [ - 'assetManager' => $this->createMock(AssetManager::class), 'escapeHtmlAttr' => new EscapeHtmlAttr($escaper), ], $plugins diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php index f2071e9c0b7..bf40fb8ff5d 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordDataFormatterTest.php @@ -89,7 +89,6 @@ protected function getViewHelpers($container): array }); $record->setDbServiceManager($serviceManager); return [ - 'assetManager' => $this->createMock(\VuFindTheme\View\Helper\AssetManager::class), 'auth' => new \VuFind\View\Helper\Root\Auth( $this->createMock(\VuFind\Auth\Manager::class), $this->createMock(\VuFind\Auth\ILSAuthenticator::class) diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php index 9a91a5d852a..6cc40db9b4b 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManagerFactory.php @@ -102,7 +102,7 @@ public function __invoke( $configManager = $container->get(\VuFind\Config\PluginManager::class); $nonceGenerator = $container->get(\VuFind\Security\NonceGenerator::class); $nonce = $nonceGenerator->getNonce(); - $config = $configManager->get('config')->toArray(); + $config = $configManager->get('config')?->toArray() ?? []; return new $requestedName( $container->get(\VuFindTheme\ThemeInfo::class), $this->getPipelineConfig($config), From b315e7494e472d168b352fdb1bb798e8b403fc10 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 06:59:18 -0500 Subject: [PATCH 18/30] Fix style; port another helper. --- .../src/VuFind/View/Helper/Root/GoogleAnalytics.php | 2 -- .../src/VuFind/View/Helper/Root/GoogleTagManager.php | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php b/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php index dc31b1eb421..e7682dc8018 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php @@ -29,8 +29,6 @@ namespace VuFind\View\Helper\Root; -use Laminas\View\Helper\HeadScript; - use function is_array; /** diff --git a/module/VuFind/src/VuFind/View/Helper/Root/GoogleTagManager.php b/module/VuFind/src/VuFind/View/Helper/Root/GoogleTagManager.php index 26cc29dfa22..2c9582b3cd0 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/GoogleTagManager.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/GoogleTagManager.php @@ -68,7 +68,6 @@ public function getHeadCode() return ''; } - // phpcs:disable -- line length should be kept for this vendor snippet $js = <<gtmContainerId}'); END; - // phpcs:enable - $inlineScript = $this->getView()->plugin('inlinescript'); - $js = $inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $js, 'SET'); - return $js; + return $this->getView()->plugin('assetManager')->outputInlineScript($js); } /** @@ -94,14 +90,12 @@ public function getBodyCode() return ''; } - // phpcs:disable -- line length should be kept for this vendor snippet $js = << END; - // phpcs:enable return $js; } } From f340000e32b5493318487fba2f43fc4bdd18b237 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 07:06:22 -0500 Subject: [PATCH 19/30] Finish porting helpers to use AssetManager. --- module/VuFind/src/VuFind/View/Helper/Root/Matomo.php | 4 +--- module/VuFind/src/VuFind/View/Helper/Root/Piwik.php | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Matomo.php b/module/VuFind/src/VuFind/View/Helper/Root/Matomo.php index ac729669195..f8467e0d7e3 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/Matomo.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/Matomo.php @@ -177,9 +177,7 @@ public function __invoke(array $params = []): string } else { $code = $this->trackPageView(); } - - $inlineScript = $this->getView()->plugin('inlinescript'); - return $inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $code, 'SET'); + return $this->getView()->plugin('assetManager')->outputInlineScript($code); } /** diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php b/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php index ac760b65131..eaa856053c1 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php @@ -174,8 +174,7 @@ public function __invoke($params = null) $code = $this->trackPageView(); } - $inlineScript = $this->getView()->plugin('inlinescript'); - return $inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $code, 'SET'); + return $this->getView()->plugin('assetManager')->outputInlineScript($code); } /** From 47db21d4aebcb466318b31fe98e9de579a8dc159 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 07:12:26 -0500 Subject: [PATCH 20/30] Refactor nonce handling. --- module/VuFindTheme/Module.php | 2 - .../VuFindTheme/View/Helper/AssetManager.php | 4 ++ .../VuFindTheme/View/Helper/ConcatTrait.php | 15 ----- .../VuFindTheme/View/Helper/HeadScript.php | 13 ---- .../VuFindTheme/View/Helper/InlineScript.php | 66 ------------------- 5 files changed, 4 insertions(+), 96 deletions(-) delete mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index ac439cc59a5..24215480da9 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -112,7 +112,6 @@ public function getViewHelperConfig() View\Helper\ImageLink::class => View\Helper\ImageLinkFactory::class, View\Helper\HeadScript::class => View\Helper\PipelineInjectorFactory::class, View\Helper\ParentTemplate::class => View\Helper\ParentTemplateFactory::class, - View\Helper\InlineScript::class => View\Helper\PipelineInjectorFactory::class, View\Helper\Slot::class => InvokableFactory::class, View\Helper\TemplatePath::class => View\Helper\TemplatePathFactory::class, View\Helper\SetupThemeResources::class => View\Helper\SetupThemeResourcesFactory::class, @@ -124,7 +123,6 @@ public function getViewHelperConfig() 'headThemeResources' => View\Helper\SetupThemeResources::class, 'imageLink' => View\Helper\ImageLink::class, \Laminas\View\Helper\HeadScript::class => View\Helper\HeadScript::class, - \Laminas\View\Helper\InlineScript::class => View\Helper\InlineScript::class, 'parentTemplate' => View\Helper\ParentTemplate::class, 'slot' => View\Helper\Slot::class, 'templatePath' => View\Helper\TemplatePath::class, diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 0dbbd3ada85..6ecbc92edae 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -311,6 +311,8 @@ protected function outputScriptAssets($position): string if ($script['allowArbitraryAttrs'] ?? false) { $scriptHelper->setAllowArbitraryAttributes(true); } + // Add nonce to output: + $script['attrs']['nonce'] = $this->cspNonce; // Every $script will have either a script attribute (inline JS) or a src attribute (file): if (isset($script['script'])) { $scriptHelper->appendScript($script['script'], $script['type'], $script['attrs']); @@ -729,6 +731,7 @@ public function outputInlineScript( array $attrs = [], bool $allowArbitraryAttrs = false, ): string { + $attrs['nonce'] = $this->cspNonce; $inlineScript = $this->getView()->plugin('inlineScript'); if ($allowArbitraryAttrs) { $inlineScript->setAllowArbitraryAttributes(true); @@ -751,6 +754,7 @@ public function outputInlineScriptFile( string $type = 'text/javascript', array $attrs = [], ): string { + $attrs['nonce'] = $this->cspNonce; $inlineScript = $this->getView()->plugin('inlineScript'); $inlineScript->setFile($src, $type, $attrs); return ($inlineScript)(); diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php index 6e674a4e4bb..7b14d541de2 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php @@ -94,20 +94,6 @@ abstract protected function setResourceFilePath($item, $path); */ abstract protected function getMinifier(); - /** - * Add a content security policy nonce to the item - * - * @param stdClass $item Item - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function addNonce($item) - { - // Default implementation does nothing - } - /** * Set the file path of the link object * @@ -403,7 +389,6 @@ protected function outputInOrder($indent) // files, which are stored in a theme-independent cache). $path = $this->getConcatenatedFilePath($group); $item = $this->setResourceFilePath($group['items'][0], $path); - $this->addNonce($item); /** * PHPStan doesn't like this because of incompatible itemToString * signatures in HeadLink/HeadScript, but it is safe to use because diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php index 7b140d9d890..9ab450d51d9 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php @@ -119,7 +119,6 @@ public function itemToString($item, $indent, $escapeStart, $escapeEnd) } } - $this->addNonce($item); return parent::itemToString($item, $indent, $escapeStart, $escapeEnd); } @@ -196,16 +195,4 @@ protected function getMinifiedData($details, $concatPath) } return $data; } - - /** - * Add a nonce to the item - * - * @param stdClass $item Item - * - * @return void - */ - protected function addNonce($item) - { - $item->attributes['nonce'] = $this->cspNonce; - } } diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php deleted file mode 100644 index d8bae7fdbad..00000000000 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFindTheme\View\Helper; - -/** - * Inline script view helper (extended for VuFind's theme system) - * - * @category VuFind - * @package View_Helpers - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class InlineScript extends HeadScript -{ - /** - * Return InlineScript object - * - * Returns InlineScript helper object; optionally, allows specifying a - * script or script file to include. - * - * @param string $mode Script or file - * @param string $spec Script/url - * @param string $placement Append, prepend, or set - * @param array $attrs Array of script attributes - * @param string $type Script type and/or array of script attributes - * - * @return InlineScript - */ - public function __invoke( - $mode = HeadScript::FILE, - $spec = null, - $placement = 'APPEND', - array $attrs = [], - $type = 'text/javascript' - ) { - return parent::__invoke($mode, $spec, $placement, $attrs, $type); - } -} From 9551cff10d46baae1dc5fafac62d1cf956a48415 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 07:53:43 -0500 Subject: [PATCH 21/30] Progress on removing internal helpers. --- config/vufind/config.ini | 6 +- module/VuFindTheme/Module.php | 5 +- .../VuFindTheme/View/Helper/AssetManager.php | 45 +- .../VuFindTheme/View/Helper/ConcatTrait.php | 476 ------------------ .../VuFindTheme/View/Helper/FootScript.php | 46 -- .../VuFindTheme/View/Helper/HeadScript.php | 151 +----- .../View/Helper/PipelineInjectorFactory.php | 114 ----- .../View/Helper/SetupThemeResourcesTest.php | 24 +- 8 files changed, 45 insertions(+), 822 deletions(-) delete mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php delete mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php delete mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/PipelineInjectorFactory.php diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 6b0e1560bfa..72043cb68cb 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -55,9 +55,9 @@ theme = sandal5 ; Uncomment the following line to use a different theme for Admin module. ;admin_theme = sandal -; Automatic asset minification and concatenation setting. When active, HeadScript -; and HeadLink will concatenate and minify all viable files to reduce requests and -; load times. This setting is off by default. +; Automatic asset minification and concatenation setting. When active, the asset +; manager helper will concatenate and minify all viable files to reduce requests +; and load times. This setting is off by default. ; ; This configuration takes the form of a semi-colon separated list of ; environment:configuration pairs where "environment" is a possible APPLICATION_ENV diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index 24215480da9..1c5fcb71476 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -108,9 +108,8 @@ public function getViewHelperConfig() return [ 'factories' => [ View\Helper\AssetManager::class => View\Helper\AssetManagerFactory::class, - View\Helper\FootScript::class => View\Helper\PipelineInjectorFactory::class, + View\Helper\HeadScript::class => InvokableFactory::class, View\Helper\ImageLink::class => View\Helper\ImageLinkFactory::class, - View\Helper\HeadScript::class => View\Helper\PipelineInjectorFactory::class, View\Helper\ParentTemplate::class => View\Helper\ParentTemplateFactory::class, View\Helper\Slot::class => InvokableFactory::class, View\Helper\TemplatePath::class => View\Helper\TemplatePathFactory::class, @@ -118,11 +117,9 @@ public function getViewHelperConfig() ], 'aliases' => [ 'assetManager' => View\Helper\AssetManager::class, - 'footScript' => View\Helper\FootScript::class, // Legacy alias for compatibility with pre-8.0 templates: 'headThemeResources' => View\Helper\SetupThemeResources::class, 'imageLink' => View\Helper\ImageLink::class, - \Laminas\View\Helper\HeadScript::class => View\Helper\HeadScript::class, 'parentTemplate' => View\Helper\ParentTemplate::class, 'slot' => View\Helper\Slot::class, 'templatePath' => View\Helper\TemplatePath::class, diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 6ecbc92edae..bbcfe0ba489 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -121,13 +121,10 @@ protected function isPipelineAvailable(): bool protected function isPipelineEnabledForType(string $fileType): bool { $config = $this->pipelineConfig; - if ($config === false || $config == 'off') { + if ($config === false || $config == 'off' || $config == 'false' || $config === '0') { return false; } - if ( - $config == '*' || $config == 'on' - || $config == 'true' || $config === true - ) { + if ($config == '*' || $config == 'on' || $config == 'true' || $config === true || $config === '1') { return true; } $settings = array_map('trim', explode(',', $config)); @@ -305,9 +302,11 @@ public function prependScript( */ protected function outputScriptAssets($position): string { - $helperName = $position === 'header' ? 'headScript' : 'footScript'; - $scriptHelper = $this->getView()->plugin($helperName); - foreach ($this->scripts[$position] as $script) { + // We can use the headScript header for every position, because each call to this method will + // set up and then clear out the contents of the helper. + $scriptHelper = $this->getView()->plugin('headScript'); + $processedScripts = $this->processForPipeline($this->scripts[$position], 'js'); + foreach ($processedScripts as $script) { if ($script['allowArbitraryAttrs'] ?? false) { $scriptHelper->setAllowArbitraryAttributes(true); } @@ -359,6 +358,10 @@ 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"); } @@ -374,10 +377,11 @@ protected function isExcludedFromConcat(array $item, string $type): bool */ protected function getResourceFilePath(array $item, string $type): string { - if ($type === 'css') { - return $item['href']; + $key = $this->getFileKeyByType($type); + if (!isset($item[$key])) { + throw new Exception("Unexpected missing $key key in $type item."); } - throw new Exception("Unknown type: $type"); + return $item[$key]; } /** @@ -396,10 +400,9 @@ protected function getGroupType(array $item, string $type): string if (isset($item['conditionalStylesheet'])) { $type .= '_' . $item['conditionalStylesheet']; } - } else { - throw new Exception("Unknown type: $type"); + return $groupType; } - return $groupType; + return 'default'; } /** @@ -484,12 +487,13 @@ 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"); + throw new Exception("Unsupported type: $type"); } - if (null !== $this->maxImportSize) { + if ($type === 'css' && null !== $this->maxImportSize) { $minifier->setMaxImportSize($this->maxImportSize); } return $minifier; @@ -520,6 +524,10 @@ protected function getMinifiedData(array $details, string $concatPath, string $t ); } } + // Play it safe by terminating Javascript code with a semicolon + if ($type === 'js' && !str_ends_with(trim($data), ';')) { + $data .= ';'; + } return $data; } @@ -615,8 +623,9 @@ protected function getConcatenatedFilePath(array $group, string $type): string */ protected function getFileKeyByType(string $type): string { - if ($type === 'css') { - return 'href'; + $keys = ['css' => 'href', 'js' => 'src']; + if (isset($keys[$type])) { + return $keys[$type]; } throw new Exception("Unexpected type: $type"); } diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php deleted file mode 100644 index 7b14d541de2..00000000000 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php +++ /dev/null @@ -1,476 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFindTheme\View\Helper; - -use VuFindTheme\ThemeInfo; - -use function count; -use function defined; -use function in_array; -use function is_resource; - -/** - * Trait to add asset pipeline functionality (concatenation / minification) to - * a HeadLink/HeadScript-style view helper. - * - * @category VuFind - * @package View_Helpers - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:testing:unit_tests Wiki - */ -trait ConcatTrait -{ - /** - * Returns true if file should not be included in the compressed concat file - * - * @param stdClass $item Element object - * - * @return bool - */ - abstract protected function isExcludedFromConcat($item); - - /** - * Get the folder name and file extension - * - * @return string - */ - abstract protected function getFileType(); - - /** - * Get the file path from the element object - * - * @param stdClass $item Element object - * - * @return string - */ - abstract protected function getResourceFilePath($item); - - /** - * Set the file path of the element object - * - * @param stdClass $item Element object - * @param string $path New path string - * - * @return stdClass - */ - abstract protected function setResourceFilePath($item, $path); - - /** - * Get the minifier that can handle these file types - * - * @return minifying object like \MatthiasMullie\Minify\JS - */ - abstract protected function getMinifier(); - - /** - * Set the file path of the link object - * - * @param stdClass $item Link element object - * - * @return string - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function getType($item) - { - return 'default'; - } - - /** - * Should we use the asset pipeline to join files together and minify them? - * - * @var bool - */ - protected $usePipeline = false; - - /** - * Array of resource items by type, contains key as well - * - * @var array - */ - protected $groups = []; - - /** - * Future order of the concatenated file - * - * @var number - */ - protected $concatIndex = null; - - /** - * Check if config is enabled for this file type - * - * @param string|bool $config Config for current application environment - * - * @return bool - */ - protected function enabledInConfig($config) - { - if ($config === false || $config == 'off') { - return false; - } - if ( - $config == '*' || $config == 'on' - || $config == 'true' || $config === true - ) { - return true; - } - $settings = array_map('trim', explode(',', $config)); - return in_array($this->getFileType(), $settings); - } - - /** - * Initialize class properties related to concatenation of resources. - * All of the elements to be concatenated into groups and - * and those that need to remain on their own special group 'other'. - * - * @return bool True if there are items - */ - protected function filterItems() - { - $this->groups = []; - $groupTypes = []; - - $this->getContainer()->ksort(); - - foreach ($this as $item) { - if ($this->isExcludedFromConcat($item)) { - $this->groups[] = [ - 'other' => true, - 'item' => $item, - ]; - $groupTypes[] = 'other'; - continue; - } - - $path = $this->getFileType() . '/' . $this->getResourceFilePath($item); - $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"; - method_exists($this, 'logError') - ? $this->logError($errorMsg) : error_log($errorMsg); - $this->groups[] = [ - 'other' => true, - 'item' => $item, - ]; - $groupTypes[] = 'other'; - continue; - } - - $type = $this->getType($item); - $index = array_search($type, $groupTypes); - if ($index === false) { - $this->groups[] = [ - 'items' => [$item], - 'key' => $details['path'] . filemtime($details['path']), - ]; - $groupTypes[] = $type; - } else { - $this->groups[$index]['items'][] = $item; - $this->groups[$index]['key'] .= - $details['path'] . filemtime($details['path']); - } - } - - return count($groupTypes) > 0; - } - - /** - * 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() - { - 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; - } - - /** - * Using the concatKey, return the path of the concatenated file. - * Generate if it does not yet exist. - * - * @param array $group Object containing 'key' and stdobj file 'items' - * - * @return string - */ - protected function getConcatenatedFilePath($group) - { - $urlHelper = $this->getView()->plugin('url'); - - // Don't recompress individual files - if (count($group['items']) === 1) { - $path = $this->getResourceFilePath($group['items'][0]); - $details = $this->themeInfo->findContainingTheme( - $this->getFileType() . '/' . $path, - ThemeInfo::RETURN_ALL_DETAILS - ); - return $urlHelper('home') . 'themes/' . $details['theme'] - . '/' . $this->getFileType() . '/' . $path; - } - // Locate/create concatenated asset file - $filename = md5($group['key']) . '.min.' . $this->getFileType(); - // 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); - } catch (\Exception $e) { - flock($handle, LOCK_UN); - fclose($handle); - throw $e; - } - } - flock($handle, LOCK_UN); - fclose($handle); - } - - return $urlHelper('home') . 'cache/' . $filename; - } - - /** - * 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' - * - * @throws \Exception - * @return void - */ - protected function createConcatenatedFile($concatPath, $group) - { - $data = []; - foreach ($group['items'] as $item) { - $details = $this->themeInfo->findContainingTheme( - $this->getFileType() . '/' - . $this->getResourceFilePath($item), - ThemeInfo::RETURN_ALL_DETAILS - ); - $details['path'] = realpath($details['path']); - $data[] = $this->getMinifiedData($details, $concatPath); - } - // 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)); - } - - /** - * 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) - * - * @throws \Exception - * @return string - */ - protected function getMinifiedData($details, $concatPath) - { - if ($this->isMinifiable($details['path'])) { - $minifier = $this->getMinifier(); - $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']}" - ); - } - } - return $data; - } - - /** - * Process and return items in index order - * - * @param string|int $indent Amount of whitespace/string to use for indentation - * - * @return string - */ - protected function outputInOrder($indent) - { - // Some of this logic was copied from HeadScript; it does not all apply - // when incorporated into HeadLink, but it has no harmful side effects. - $indent = (null !== $indent) - ? $this->getWhitespace($indent) - : $this->getIndent(); - - if ($this->view) { - $useCdata = $this->view->plugin('doctype')->isXhtml(); - } else { - $useCdata = $this->useCdata ?? false; - } - - $escapeStart = ($useCdata) ? '//' : '//-->'; - - $output = []; - foreach ($this->groups as $group) { - if (isset($group['other'])) { - /** - * PHPStan doesn't like this because of incompatible itemToString - * signatures in HeadLink/HeadScript, but it is safe to use because - * the extra parameters will be ignored appropriately. - * - * @phpstan-ignore-next-line - */ - $output[] = $this->itemToString( - $group['item'], - $indent, - $escapeStart, - $escapeEnd - ); - } else { - // Note that we use parent::itemToString() below instead of - // $this->itemToString() to bypass VuFind logic that determines - // file paths within the theme (not appropriate for concatenated - // files, which are stored in a theme-independent cache). - $path = $this->getConcatenatedFilePath($group); - $item = $this->setResourceFilePath($group['items'][0], $path); - /** - * PHPStan doesn't like this because of incompatible itemToString - * signatures in HeadLink/HeadScript, but it is safe to use because - * the extra parameters will be ignored appropriately. - * - * @phpstan-ignore-next-line - */ - $output[] = parent::itemToString( - $item, - $indent, - $escapeStart, - $escapeEnd - ); - } - } - - return $indent . implode( - $this->escape($this->getSeparator()) . $indent, - $output - ); - } - - /** - * 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($filename) - { - $basename = basename($filename); - return preg_match('/\.min\.(js|css)/', $basename) === 0; - } - - /** - * Can we use the asset pipeline? - * - * @return bool - */ - protected function isPipelineActive() - { - if ($this->usePipeline) { - try { - $cacheDir = $this->getResourceCacheDir(); - } catch (\Exception $e) { - $this->usePipeline = $cacheDir = false; - error_log($e->getMessage()); - } - if ($cacheDir && !is_writable($cacheDir)) { - $this->usePipeline = false; - error_log("Cannot write to $cacheDir; disabling asset pipeline."); - } - } - return $this->usePipeline; - } - - /** - * Render link elements as string - * Customized to minify and concatenate - * - * @param string|int $indent Amount of whitespace or string to use for indentation - * - * @return string - */ - public function toString($indent = null) - { - // toString must not throw exception - try { - if ( - !$this->isPipelineActive() || !$this->filterItems() - || count($this) == 1 - ) { - return parent::toString($indent); - } - - return $this->outputInOrder($indent); - } catch (\Exception $e) { - error_log($e->getMessage()); - } - - return ''; - } -} diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php deleted file mode 100644 index 1b1f9af9cb3..00000000000 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php +++ /dev/null @@ -1,46 +0,0 @@ -) - * - * PHP version 8 - * - * Copyright (C) Villanova University 2021. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * @category VuFind - * @package View_Helpers - * @author Chris Hallberg - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFindTheme\View\Helper; - -/** - * Footer script view helper (same as HeadScript but outputs to the bottom of ) - * - * @category VuFind - * @package View_Helpers - * @author Chris Hallberg - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class FootScript extends HeadScript -{ - // pass -} diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php index 9ab450d51d9..1e1e4ba5b6b 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php @@ -44,155 +44,6 @@ * @method getIndent() * @method getSeparator() */ -class HeadScript extends \Laminas\View\Helper\HeadScript implements \Laminas\Log\LoggerAwareInterface +class HeadScript extends \Laminas\View\Helper\HeadScript { - use ConcatTrait { - getMinifiedData as getBaseMinifiedData; - } - use RelativePathTrait; - use \VuFind\Log\LoggerAwareTrait; - - /** - * Theme information service - * - * @var ThemeInfo - */ - protected $themeInfo; - - /** - * CSP nonce - * - * @var string - */ - protected $cspNonce; - - /** - * Constructor - * - * @param ThemeInfo $themeInfo Theme information service - * @param string|bool $plconfig Config for current application environment - * @param string $nonce Nonce from nonce generator - */ - public function __construct(ThemeInfo $themeInfo, $plconfig = false, $nonce = '') - { - parent::__construct(); - $this->themeInfo = $themeInfo; - $this->usePipeline = $this->enabledInConfig($plconfig); - $this->cspNonce = $nonce; - $this->optionalAttributes[] = 'nonce'; - } - - /** - * Folder name and file extension for trait - * - * @return string - */ - protected function getFileType() - { - return 'js'; - } - - /** - * Create script HTML - * - * @param mixed $item Item to convert - * @param string $indent String to add before the item - * @param string $escapeStart Starting sequence - * @param string $escapeEnd Ending sequence - * - * @return string - */ - public function itemToString($item, $indent, $escapeStart, $escapeEnd) - { - // Normalize href to account for themes (if appropriate): - if (!empty($item->attributes['src']) && $this->isRelativePath($item->attributes['src'])) { - $relPath = 'js/' . $item->attributes['src']; - $details = $this->themeInfo - ->findContainingTheme($relPath, ThemeInfo::RETURN_ALL_DETAILS); - - if (!empty($details)) { - $urlHelper = $this->getView()->plugin('url'); - $url = $urlHelper('home') . "themes/{$details['theme']}/" . $relPath; - $url .= strstr($url, '?') ? '&_=' : '?_='; - $url .= filemtime($details['path']); - $item->attributes['src'] = $url; - } - } - - return parent::itemToString($item, $indent, $escapeStart, $escapeEnd); - } - - /** - * Returns true if file should not be included in the compressed concat file - * Required by ConcatTrait - * - * @param stdClass $item Script element object - * - * @return bool - */ - protected function isExcludedFromConcat($item) - { - return empty($item->attributes['src']) - || isset($item->attributes['conditional']) - || !$this->isRelativePath($item->attributes['src']); - } - - /** - * Get the file path from the script object - * Required by ConcatTrait - * - * @param stdClass $item Script element object - * - * @return string - */ - protected function getResourceFilePath($item) - { - return $item->attributes['src']; - } - - /** - * Set the file path of the script object - * Required by ConcatTrait - * - * @param stdClass $item Script element object - * @param string $path New path string - * - * @return stdClass - */ - protected function setResourceFilePath($item, $path) - { - $item->attributes['src'] = $path; - return $item; - } - - /** - * Get the minifier that can handle these file types - * Required by ConcatTrait - * - * @return \MatthiasMullie\Minify\JS - */ - protected function getMinifier() - { - return new \MatthiasMullie\Minify\JS(); - } - - /** - * 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) - * - * @throws \Exception - * @return string - */ - protected function getMinifiedData($details, $concatPath) - { - $data = $this->getBaseMinifiedData($details, $concatPath); - // Play it safe by terminating a script with a semicolon - if (!str_ends_with(trim($data), ';')) { - $data .= ';'; - } - return $data; - } } diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/PipelineInjectorFactory.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/PipelineInjectorFactory.php deleted file mode 100644 index ebcc385a40a..00000000000 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/PipelineInjectorFactory.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFindTheme\View\Helper; - -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 VuFind\Config\Config; - -use function count; - -/** - * Factory for helpers relying on asset pipeline configuration. - * - * @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 PipelineInjectorFactory implements FactoryInterface -{ - /** - * Split config and return prefixed setting with current environment. - * - * @param Config $config Configuration settings - * - * @return string|bool - */ - protected function getPipelineConfig(Config $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); - $nonceGenerator = $container->get(\VuFind\Security\NonceGenerator::class); - $nonce = $nonceGenerator->getNonce(); - $config = $configManager->get('config'); - return new $requestedName( - $container->get(\VuFindTheme\ThemeInfo::class), - $this->getPipelineConfig($config), - $nonce, - $config['Site']['asset_pipeline_max_css_import_size'] ?? null - ); - } -} diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/SetupThemeResourcesTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/SetupThemeResourcesTest.php index d09c48a9a88..e690b40a04e 100644 --- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/SetupThemeResourcesTest.php +++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/SetupThemeResourcesTest.php @@ -29,6 +29,10 @@ namespace VuFindTest\View\Helper; +use Laminas\View\Helper\HeadLink; +use Laminas\View\Helper\HeadMeta; +use Laminas\View\Helper\HeadScript; +use PHPUnit\Framework\MockObject\MockObject; use VuFindTheme\ResourceContainer; use VuFindTheme\View\Helper\SetupThemeResources; @@ -108,9 +112,9 @@ protected function getMockView() /** * Get a fake HeadMeta helper. * - * @return \PHPUnit\Framework\MockObject\MockObject&\VuFindTheme\View\Helper\HeadMeta + * @return MockObject&HeadMeta */ - protected function getMockHeadMeta() + protected function getMockHeadMeta(): MockObject&HeadMeta { $mock = $this->getMockBuilder(\Laminas\View\Helper\HeadMeta::class) ->disableOriginalConstructor() @@ -129,11 +133,11 @@ protected function getMockHeadMeta() /** * Get a fake HeadLink helper. * - * @return \Laminas\View\Helper\HeadLink + * @return MockObject&HeadLink */ - protected function getMockHeadLink() + protected function getMockHeadLink(): MockObject&HeadLink { - $mock = $this->createMock(\Laminas\View\Helper\HeadLink::class); + $mock = $this->createMock(HeadLink::class); $mock->expects($this->any())->method('__invoke')->willReturn($mock); return $mock; } @@ -141,14 +145,12 @@ protected function getMockHeadLink() /** * Get a fake HeadScript helper. * - * @return \Laminas\View\Helper\HeadScript + * @return MockObject&HeadScript */ - protected function getMockHeadScript() + protected function getMockHeadScript(): MockObject&HeadScript { - $mock = $this->getMockBuilder(\VuFindTheme\View\Helper\HeadScript::class) - ->disableOriginalConstructor() - ->getMock(); - $mock->expects($this->any())->method('__invoke')->will($this->returnValue($mock)); + $mock = $this->createMock(HeadScript::class); + $mock->expects($this->any())->method('__invoke')->willReturn($mock); return $mock; } } From 5ee33b4e5ae7206e93a331264289562d6c079a14 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 07:58:52 -0500 Subject: [PATCH 22/30] Only include non-empty nonce. --- .../src/VuFindTheme/View/Helper/AssetManager.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index bbcfe0ba489..9a5fed48305 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -311,7 +311,9 @@ protected function outputScriptAssets($position): string $scriptHelper->setAllowArbitraryAttributes(true); } // Add nonce to output: - $script['attrs']['nonce'] = $this->cspNonce; + if (!empty($this->cspNonce)) { + $script['attrs']['nonce'] = $this->cspNonce; + } // Every $script will have either a script attribute (inline JS) or a src attribute (file): if (isset($script['script'])) { $scriptHelper->appendScript($script['script'], $script['type'], $script['attrs']); @@ -740,7 +742,9 @@ public function outputInlineScript( array $attrs = [], bool $allowArbitraryAttrs = false, ): string { - $attrs['nonce'] = $this->cspNonce; + if (!empty($this->cspNonce)) { + $attrs['nonce'] = $this->cspNonce; + } $inlineScript = $this->getView()->plugin('inlineScript'); if ($allowArbitraryAttrs) { $inlineScript->setAllowArbitraryAttributes(true); @@ -763,7 +767,9 @@ public function outputInlineScriptFile( string $type = 'text/javascript', array $attrs = [], ): string { - $attrs['nonce'] = $this->cspNonce; + if (!empty($this->cspNonce)) { + $attrs['nonce'] = $this->cspNonce; + } $inlineScript = $this->getView()->plugin('inlineScript'); $inlineScript->setFile($src, $type, $attrs); return ($inlineScript)(); From 19acff9e954160be1ce80a0bc7a93766e8e15816 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 08:16:37 -0500 Subject: [PATCH 23/30] Various fixes. --- module/VuFindTheme/Module.php | 2 + .../VuFindTheme/View/Helper/AssetManager.php | 42 +++++++++++----- .../VuFindTheme/View/Helper/FootScript.php | 48 +++++++++++++++++++ .../VuFindTheme/View/Helper/HeadScript.php | 2 - 4 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index 1c5fcb71476..0709388ab6f 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -108,6 +108,7 @@ public function getViewHelperConfig() return [ 'factories' => [ View\Helper\AssetManager::class => View\Helper\AssetManagerFactory::class, + View\Helper\FootScript::class => InvokableFactory::class, View\Helper\HeadScript::class => InvokableFactory::class, View\Helper\ImageLink::class => View\Helper\ImageLinkFactory::class, View\Helper\ParentTemplate::class => View\Helper\ParentTemplateFactory::class, @@ -117,6 +118,7 @@ public function getViewHelperConfig() ], 'aliases' => [ 'assetManager' => View\Helper\AssetManager::class, + 'footScript' => View\Helper\HeadScript::class, // Legacy alias for compatibility with pre-8.0 templates: 'headThemeResources' => View\Helper\SetupThemeResources::class, 'imageLink' => View\Helper\ImageLink::class, diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 9a5fed48305..52d93de2011 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -293,6 +293,28 @@ public function prependScript( array_unshift($this->scripts[$position], compact('script', 'type', 'attrs', 'allowArbitraryAttrs')); } + /** + * Given a relative JS or CSS path, apply appropriate theme prefixing if possible; return null if + * the resource could not be found in a theme. + * + * @param string $relPath Relative path to find in theme + * + * @return ?string + */ + protected function applyThemeToRelativePath(string $relPath): ?string + { + $details = $this->themeInfo->findContainingTheme($relPath, ThemeInfo::RETURN_ALL_DETAILS); + if (!empty($details)) { + $urlHelper = $this->getView()->plugin('url'); + $url = $urlHelper('home') . "themes/{$details['theme']}/" . $relPath; + $url .= strstr($url, '?') ? '&_=' : '?_='; + $url .= filemtime($details['path']); + return $url; + } + // Cannot find in theme? Return null. + return null; + } + /** * Return the HTML to output script assets in the requested position. * @@ -302,9 +324,8 @@ public function prependScript( */ protected function outputScriptAssets($position): string { - // We can use the headScript header for every position, because each call to this method will - // set up and then clear out the contents of the helper. - $scriptHelper = $this->getView()->plugin('headScript'); + $helperName = $position === 'header' ? 'headScript' : 'footScript'; + $scriptHelper = $this->getView()->plugin($helperName); $processedScripts = $this->processForPipeline($this->scripts[$position], 'js'); foreach ($processedScripts as $script) { if ($script['allowArbitraryAttrs'] ?? false) { @@ -318,6 +339,11 @@ protected function outputScriptAssets($position): string if (isset($script['script'])) { $scriptHelper->appendScript($script['script'], $script['type'], $script['attrs']); } else { + if ($this->isRelativePath($script['src'])) { + if ($themePath = $this->applyThemeToRelativePath('js/' . $script['src'])) { + $script['src'] = $themePath; + } + } $scriptHelper->appendFile($script['src'], $script['type'], $script['attrs']); } } @@ -689,14 +715,8 @@ protected function outputStyleAssets(): string foreach ($processedStylesheets as $sheet) { // Account for the theme system (when appropriate): if ($this->isRelativePath($sheet['href'])) { - $relPath = 'css/' . $sheet['href']; - $details = $this->themeInfo->findContainingTheme($relPath, ThemeInfo::RETURN_ALL_DETAILS); - if (!empty($details)) { - $urlHelper = $this->getView()->plugin('url'); - $url = $urlHelper('home') . "themes/{$details['theme']}/" . $relPath; - $url .= strstr($url, '?') ? '&_=' : '?_='; - $url .= filemtime($details['path']); - $sheet['href'] = $url; + if ($themePath = $this->applyThemeToRelativePath('css/' . $sheet['href'])) { + $sheet['href'] = $themePath; } } diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php new file mode 100644 index 00000000000..407fd5b340e --- /dev/null +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php @@ -0,0 +1,48 @@ +) + * + * PHP version 8 + * + * Copyright (C) Villanova University 2021. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package View_Helpers + * @author Chris Hallberg + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace VuFindTheme\View\Helper; + +use Laminas\View\Helper\HeadScript; + +/** + * Footer script view helper (same as HeadScript but outputs to the bottom of ) + * + * @category VuFind + * @package View_Helpers + * @author Chris Hallberg + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class FootScript extends HeadScript +{ + // pass +} diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php index 1e1e4ba5b6b..5f20744cdc3 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php @@ -29,8 +29,6 @@ namespace VuFindTheme\View\Helper; -use VuFindTheme\ThemeInfo; - /** * Head script view helper (extended for VuFind's theme system) * From af33c29170f3637427810c014c772f0e3063cf50 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 08:18:28 -0500 Subject: [PATCH 24/30] Remove obsolete helper. --- module/VuFindTheme/Module.php | 3 +- .../VuFindTheme/View/Helper/HeadScript.php | 47 ------------------- 2 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index 0709388ab6f..602a7ac008b 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -109,7 +109,6 @@ public function getViewHelperConfig() 'factories' => [ View\Helper\AssetManager::class => View\Helper\AssetManagerFactory::class, View\Helper\FootScript::class => InvokableFactory::class, - View\Helper\HeadScript::class => InvokableFactory::class, View\Helper\ImageLink::class => View\Helper\ImageLinkFactory::class, View\Helper\ParentTemplate::class => View\Helper\ParentTemplateFactory::class, View\Helper\Slot::class => InvokableFactory::class, @@ -118,7 +117,7 @@ public function getViewHelperConfig() ], 'aliases' => [ 'assetManager' => View\Helper\AssetManager::class, - 'footScript' => View\Helper\HeadScript::class, + 'footScript' => View\Helper\FootScript::class, // Legacy alias for compatibility with pre-8.0 templates: 'headThemeResources' => View\Helper\SetupThemeResources::class, 'imageLink' => View\Helper\ImageLink::class, diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php deleted file mode 100644 index 5f20744cdc3..00000000000 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFindTheme\View\Helper; - -/** - * Head script view helper (extended for VuFind's theme system) - * - * @category VuFind - * @package View_Helpers - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - * - * @method getWhitespace(string|int $indent) - * @method getIndent() - * @method getSeparator() - */ -class HeadScript extends \Laminas\View\Helper\HeadScript -{ -} From 9535231689f698405e3207b2a7adb793f14eaa68 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 08:42:19 -0500 Subject: [PATCH 25/30] Fix inline script output. --- .../VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 52d93de2011..395117646b3 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -791,6 +791,9 @@ public function outputInlineScriptFile( $attrs['nonce'] = $this->cspNonce; } $inlineScript = $this->getView()->plugin('inlineScript'); + if ($this->isRelativePath($src)) { + $src = $this->applyThemeToRelativePath('js/' . $src) ?? $src; + } $inlineScript->setFile($src, $type, $attrs); return ($inlineScript)(); } From 252ba4bc1dac985af4c34cb1ad8abe1460192ced Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 15:14:18 -0500 Subject: [PATCH 26/30] Prevent asset pipeline duplication. --- module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 395117646b3..598a92fae56 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -480,7 +480,7 @@ protected function groupAssets(array $assets, string $type): array 'items' => [$item], 'key' => $details['path'] . filemtime($details['path']), ]; - } else { + } elseif (!in_array($item, $groups[$index]['items'])) { $groups[$index]['items'][] = $item; $groups[$index]['key'] .= $details['path'] . filemtime($details['path']); } From fa002e68215f61038944ae4825d1f4f42f580153 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 15:44:47 -0500 Subject: [PATCH 27/30] Eliminate FootScript helper. --- module/VuFindTheme/Module.php | 2 - .../VuFindTheme/View/Helper/AssetManager.php | 12 ++--- .../VuFindTheme/View/Helper/FootScript.php | 48 ------------------- 3 files changed, 6 insertions(+), 56 deletions(-) delete mode 100644 module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php index 602a7ac008b..cccb70e144f 100644 --- a/module/VuFindTheme/Module.php +++ b/module/VuFindTheme/Module.php @@ -108,7 +108,6 @@ public function getViewHelperConfig() return [ 'factories' => [ View\Helper\AssetManager::class => View\Helper\AssetManagerFactory::class, - View\Helper\FootScript::class => InvokableFactory::class, View\Helper\ImageLink::class => View\Helper\ImageLinkFactory::class, View\Helper\ParentTemplate::class => View\Helper\ParentTemplateFactory::class, View\Helper\Slot::class => InvokableFactory::class, @@ -117,7 +116,6 @@ public function getViewHelperConfig() ], 'aliases' => [ 'assetManager' => View\Helper\AssetManager::class, - 'footScript' => View\Helper\FootScript::class, // Legacy alias for compatibility with pre-8.0 templates: 'headThemeResources' => View\Helper\SetupThemeResources::class, 'imageLink' => View\Helper\ImageLink::class, diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index 598a92fae56..fdc6a510501 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -324,10 +324,10 @@ protected function applyThemeToRelativePath(string $relPath): ?string */ protected function outputScriptAssets($position): string { - $helperName = $position === 'header' ? 'headScript' : 'footScript'; - $scriptHelper = $this->getView()->plugin($helperName); + $output = []; + $scriptHelper = $this->getView()->plugin('inlineScript'); $processedScripts = $this->processForPipeline($this->scripts[$position], 'js'); - foreach ($processedScripts as $script) { + foreach ($processedScripts as $i => $script) { if ($script['allowArbitraryAttrs'] ?? false) { $scriptHelper->setAllowArbitraryAttributes(true); } @@ -337,17 +337,17 @@ protected function outputScriptAssets($position): string } // Every $script will have either a script attribute (inline JS) or a src attribute (file): if (isset($script['script'])) { - $scriptHelper->appendScript($script['script'], $script['type'], $script['attrs']); + $output[] = $this->outputInlineScript($script['script'], $script['type'], $script['attrs']); } else { if ($this->isRelativePath($script['src'])) { if ($themePath = $this->applyThemeToRelativePath('js/' . $script['src'])) { $script['src'] = $themePath; } } - $scriptHelper->appendFile($script['src'], $script['type'], $script['attrs']); + $output[] = $this->outputInlineScriptFile($script['src'], $script['type'], $script['attrs']); } } - return ($scriptHelper)(); + return implode("\n", $output); } /** diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php deleted file mode 100644 index 407fd5b340e..00000000000 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/FootScript.php +++ /dev/null @@ -1,48 +0,0 @@ -) - * - * PHP version 8 - * - * Copyright (C) Villanova University 2021. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * @category VuFind - * @package View_Helpers - * @author Chris Hallberg - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFindTheme\View\Helper; - -use Laminas\View\Helper\HeadScript; - -/** - * Footer script view helper (same as HeadScript but outputs to the bottom of ) - * - * @category VuFind - * @package View_Helpers - * @author Chris Hallberg - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class FootScript extends HeadScript -{ - // pass -} From 05433629eb0028ff6926fe6b40d73227bbaad0a8 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 4 Feb 2025 15:48:00 -0500 Subject: [PATCH 28/30] Upgrade laminas-view. --- composer.json | 2 +- composer.lock | 52 +++++++++++++++++++++++++-------------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index f4f11f1455c..c7a4383ec50 100644 --- a/composer.json +++ b/composer.json @@ -84,7 +84,7 @@ "laminas/laminas-session": "2.21.0", "laminas/laminas-stdlib": "3.19.0", "laminas/laminas-validator": "2.64.2", - "laminas/laminas-view": "2.27.0", + "laminas/laminas-view": "2.36.0", "laminas/laminas-translator": "^1", "league/commonmark": "2.6.0", "league/oauth2-client": "^2.7", diff --git a/composer.lock b/composer.lock index a6653927b63..72caf38e931 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d1765a7b30d0a5f737a778342035f65a", + "content-hash": "f92582e109e229436cb116a0b635cde6", "packages": [ { "name": "ahand/mobileesp", @@ -4482,16 +4482,16 @@ }, { "name": "laminas/laminas-view", - "version": "2.27.0", + "version": "2.36.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-view.git", - "reference": "b7e66e148ccd55c815b9626ee0cfd358dbb28be4" + "reference": "ddc9207725cb50508ea48fcf1210dc8480264196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-view/zipball/b7e66e148ccd55c815b9626ee0cfd358dbb28be4", - "reference": "b7e66e148ccd55c815b9626ee0cfd358dbb28be4", + "url": "https://api.github.com/repos/laminas/laminas-view/zipball/ddc9207725cb50508ea48fcf1210dc8480264196", + "reference": "ddc9207725cb50508ea48fcf1210dc8480264196", "shasum": "" }, "require": { @@ -4501,9 +4501,9 @@ "laminas/laminas-escaper": "^2.5", "laminas/laminas-eventmanager": "^3.4", "laminas/laminas-json": "^3.3", - "laminas/laminas-servicemanager": "^3.14.0", + "laminas/laminas-servicemanager": "^3.21.0", "laminas/laminas-stdlib": "^3.10.1", - "php": "~8.0.0 || ~8.1.0 || ~8.2.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", "psr/container": "^1 || ^2" }, "conflict": { @@ -4513,24 +4513,24 @@ "zendframework/zend-view": "*" }, "require-dev": { - "laminas/laminas-authentication": "^2.13", + "laminas/laminas-authentication": "^2.18", "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-feed": "^2.20", - "laminas/laminas-filter": "^2.31", - "laminas/laminas-http": "^2.18", - "laminas/laminas-i18n": "^2.21", - "laminas/laminas-modulemanager": "^2.14", - "laminas/laminas-mvc": "^3.6", - "laminas/laminas-mvc-i18n": "^1.7", - "laminas/laminas-mvc-plugin-flashmessenger": "^1.9", - "laminas/laminas-navigation": "^2.18.1", - "laminas/laminas-paginator": "^2.17", - "laminas/laminas-permissions-acl": "^2.13", - "laminas/laminas-router": "^3.11.1", - "laminas/laminas-uri": "^2.10", - "phpunit/phpunit": "^9.5.28", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.4" + "laminas/laminas-feed": "^2.23", + "laminas/laminas-filter": "^2.39", + "laminas/laminas-http": "^2.20", + "laminas/laminas-i18n": "^2.29.0", + "laminas/laminas-modulemanager": "^2.17", + "laminas/laminas-mvc": "^3.8.0", + "laminas/laminas-mvc-i18n": "^1.9", + "laminas/laminas-mvc-plugin-flashmessenger": "^1.10.1", + "laminas/laminas-navigation": "^2.20.0", + "laminas/laminas-paginator": "^2.19.0", + "laminas/laminas-permissions-acl": "^2.16", + "laminas/laminas-router": "^3.14.0", + "laminas/laminas-uri": "^2.12", + "phpunit/phpunit": "^10.5.38", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" }, "suggest": { "laminas/laminas-authentication": "Laminas\\Authentication component", @@ -4578,7 +4578,7 @@ "type": "community_bridge" } ], - "time": "2023-02-09T16:07:15+00:00" + "time": "2024-11-21T17:42:20+00:00" }, { "name": "lcobucci/clock", @@ -14409,7 +14409,7 @@ "platform": { "php": ">=8.1" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "8.1" }, From 13ff95887f43a3237cc442bf4be82a908f3d379f Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Wed, 5 Feb 2025 06:54:46 -0500 Subject: [PATCH 29/30] Remove redundant nonce setting. --- .../VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php index fdc6a510501..89e2cfc22bb 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/AssetManager.php @@ -331,10 +331,6 @@ protected function outputScriptAssets($position): string if ($script['allowArbitraryAttrs'] ?? false) { $scriptHelper->setAllowArbitraryAttributes(true); } - // Add nonce to output: - if (!empty($this->cspNonce)) { - $script['attrs']['nonce'] = $this->cspNonce; - } // Every $script will have either a script attribute (inline JS) or a src attribute (file): if (isset($script['script'])) { $output[] = $this->outputInlineScript($script['script'], $script['type'], $script['attrs']); From b3542d1d2de6e10ff0b647db34c2915ed7b60514 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Wed, 5 Feb 2025 07:18:10 -0500 Subject: [PATCH 30/30] 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 ); } }