diff --git a/src/PageOutput/ViewModeConfigurator.php b/src/PageOutput/ViewModeConfigurator.php index a5aabe2e..1e8539ea 100644 --- a/src/PageOutput/ViewModeConfigurator.php +++ b/src/PageOutput/ViewModeConfigurator.php @@ -16,7 +16,11 @@ namespace Horde\Core\PageOutput; +use Horde\Core\Assets\CssDiscoverer; +use Horde\Core\Assets\CssDiscoveryRequest; use Horde\Core\Assets\JsDiscoverer; +use Horde\Core\Assets\JsDiscoveryRequest; +use Horde\Core\Assets\ThemeResolver; use Horde\Core\Service\PrefsService; use Horde\Core\Session\HordeSession; use Horde\Core\Session\SessionAccess; @@ -45,8 +49,78 @@ public function __construct( private readonly SessionAccess $session, private readonly JsDiscoverer $jsDiscoverer, private readonly Token $tokenService, + private readonly ?CssDiscoverer $cssDiscoverer = null, + private readonly ?ThemeResolver $themeResolver = null, ) {} + /** + * Adds themed stylesheets to the output. + * + * Resolves the user's theme and walks the asset cascade, so a theme that + * does not ship $files still gets the default theme's version, and one + * that does gets it layered on top. + * + * Controllers outside the Horde_PageOutput path used to hardcode + * '/default/' in the stylesheet URI, which pinned those pages to the + * default theme whatever the user had selected. + * + * @param list $files Stylesheet names, e.g. ['screen.css']. + */ + public function addThemeStylesheets( + AssetCollector $collector, + array $files, + string $app = 'horde', + ): void { + if ($this->cssDiscoverer === null || $this->themeResolver === null) { + return; + } + + $theme = $this->resolveTheme(); + + $result = $this->cssDiscoverer->discover( + new CssDiscoveryRequest(files: $files, app: $app, theme: $theme) + ); + + foreach ($result as $entry) { + $collector->addStylesheet($entry->uri); + } + } + + /** + * Adds the scripts the active theme ships for itself (info.php + * $theme_scripts), as DesktopChromeRenderer does. + * + * Call it AFTER configure(): theme scripts may rely on the core + * scripts (prototype.js, horde.js) it adds. + */ + public function addThemeScripts(AssetCollector $collector, string $app = 'horde'): void + { + if ($this->themeResolver === null) { + return; + } + + $scripts = $this->jsDiscoverer->discoverTheme( + new JsDiscoveryRequest(app: $app, theme: $this->resolveTheme()) + ); + foreach ($scripts as $entry) { + $collector->addScript($entry->uri); + } + } + + /** + * The theme is a Horde-wide preference: resolve it in the horde scope, + * as DesktopChromeRendererFactory does, whatever app the assets belong + * to. $app only selects which app's files are discovered. + */ + private function resolveTheme(): string + { + $uid = $this->session->getAuthId() ?? ''; + + return $uid === '' + ? 'default' + : $this->themeResolver->resolve($uid); + } + public function configure(AssetCollector $collector, ViewMode $mode): void { $this->addBasicScripts($collector); diff --git a/src/PageOutput/ViewModeConfiguratorFactory.php b/src/PageOutput/ViewModeConfiguratorFactory.php index 3f349cb7..43ad1653 100644 --- a/src/PageOutput/ViewModeConfiguratorFactory.php +++ b/src/PageOutput/ViewModeConfiguratorFactory.php @@ -16,7 +16,9 @@ namespace Horde\Core\PageOutput; +use Horde\Core\Assets\CssDiscoverer; use Horde\Core\Assets\JsDiscoverer; +use Horde\Core\Assets\ThemeResolver; use Horde\Core\Service\PrefsService; use Horde\Core\Session\SessionAccess; use Horde_Injector; @@ -42,6 +44,8 @@ public function create(Horde_Injector|Injector $injector): ViewModeConfigurator $injector->get(SessionAccess::class), $injector->get(JsDiscoverer::class), $injector->get(Token::class), + $injector->get(CssDiscoverer::class), + $injector->get(ThemeResolver::class), ); } }