Skip to content

Let a theme ship its own javascript files - #221

Merged
ralflang merged 1 commit into
horde:FRAMEWORK_6_0from
pierrefardel:feature/theme-scripts
Aug 26, 2026
Merged

Let a theme ship its own javascript files#221
ralflang merged 1 commit into
horde:FRAMEWORK_6_0from
pierrefardel:feature/theme-scripts

Conversation

@pierrefardel

Copy link
Copy Markdown
Contributor

Let a theme ship its own javascript files

Summary

Themes can already ship CSS, images and sounds. They cannot ship javascript.
This adds $theme_scripts to info.php, so a theme can declare the behaviour
that goes with its markup:

$theme_scripts = array('theme.js');

The files are resolved inside the theme directory and added to the page
alongside the theme's stylesheets. Themes without the declaration are
unaffected.

Why

Horde_Themes_Element already covers CSS (Horde_Themes_Css), images
(Horde_Themes_Image) and sounds (Horde_Themes_Sound). Javascript is the one
asset class missing, and there is no way to fill the gap from within a theme:
Horde has no JS equivalent of the cssfiles hook.

For a theme that only changes colours this does not matter. It does as soon as a
theme restructures the shell, because some things no CSS selector can express —
wrapping a bare text node so it can be styled, or grouping siblings the
application emits as a flat list.

Today the only way to ship such a theme is to ask whoever installs it to add a
hook to their hooks.php, an instance configuration file that is not part of
the theme. The theme is then no longer installable as-is: composer require it,
enable it, and half of it silently does not load.

What the patch does

  • Horde_Themes_Cache::themeScripts() — reads $theme_scripts from the
    theme's info.php and returns the declared files. The info.php read is
    factored out of _coveredApps() into _themeInfo(), so both declarations
    share one guarded include instead of two.
  • Horde_Script_File_ThemeDir — a Horde_Script_File that resolves against
    themesfs/themesuri, mirroring Horde_Script_File_JsDir which resolves
    against an app's js/ directory.
  • Horde_PageOutput::_addThemeScripts() — emits the files, called from
    header() right where the theme is already consulted for stylesheets.

Roughly 30 lines of new logic; the rest mirrors code that already exists.

Scope and safety

Theme-directory files only. A declared entry must be a plain *.js file
name — no directory separators, no .., no URL. A theme cannot pull a script
from an arbitrary path or an external host, so this adds no CSP surface and no
third-party dependency. Unreadable files are skipped rather than emitted as
broken tags.

No new trust. Installing a theme already means executing its PHP: info.php
is included by Horde_Themes_Cache (as of the $theme_covers support), and
a theme's CSS is already arbitrary. A theme is trusted code, like an installed
application; letting it ship a script file is strictly less powerful than what
it can already do.

Theme name guard reused. Theme names come from user prefs, so _themeInfo()
keeps the existing preg_match('/^[A-Za-z0-9_-]+$/') guard before including
info.php.

Caching. themeScripts() is derived on demand and is not part of
__serialize(), so a cached Horde_Themes_Cache recomputes it — same approach
as _coveredApps().

Backwards compatible. No declaration means no script, which is the current
behaviour for every existing theme.

A note for reviewers testing locally

This adds a new class file, so a composer dump-autoload is needed before the
patch does anything: without it _addThemeScripts() fatals on a missing
Horde_Script_File_ThemeDir. Nothing to do in a normal install, where the class
ships with the horde/core package.

Testing

Exercised in a real install, dynamic view, with a theme declaring two scripts.
The install runs Core e66c4242; both files this patch touches are
byte-identical there and at the branch point, so the change under test is the
one proposed here.

The declared files are emitted alongside the core scripts and, unlike a
hook-loaded external file, they go through the normal script pipeline:

"/js/horde/accesskeys.js?v=5aae26c…",
"/themes/horde/default-new/footer.js?v=5aae26c…",
"/themes/horde/default-new/display-prefs.js?v=5aae26c…",

They are versioned like any other script file, so theme javascript takes part in
Horde's cache busting — an external-URL workaround does not, and stays cached in
the browser after the theme is updated.

Themes without $theme_scripts (default, dark) emit no additional script
tags.

The name filter was checked separately against sub/dir.js,
../../etc/passwd.js, ..js, http://evil.tld/x.js, style.css,
foo.js.php, a b.js and the empty string: all rejected, only plain *.js
names accepted.

Context

Follows the CSS custom property work in horde/base#111, horde/imp#69 and
horde/kronolith#73, and the $theme_covers declaration in #179. Those
made the layout and the calendar colours themable from CSS; this closes the
remaining gap for themes that also need to ship behaviour.

Themes can already ship CSS, images and sounds, but not javascript, and
there is no JS equivalent of the cssfiles hook to fill the gap from
within a theme.

A theme now declares its scripts in info.php:

    $theme_scripts = array('theme.js');

The files are resolved inside the theme directory and added to the page
alongside the theme's stylesheets, so they take part in the normal
script pipeline (versioning, cache busting) instead of having to be
wired up as external URLs by whoever installs the theme.

Only plain *.js file names are accepted: no directory separators, no
'..', no URL. A theme cannot pull in a script from an arbitrary path or
an external host. Unreadable files are skipped.

The info.php read is factored out of _coveredApps() into _themeInfo(),
so both declarations share one guarded include, cached per instance.
Like the covered-apps list, the script list is derived on demand and is
not serialized.

Themes without the declaration are unaffected.
Copilot AI lite review requested due to automatic review settings August 24, 2026 09:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for themes to ship and register their own JavaScript assets, analogous to existing theme support for CSS/images/sounds. This integrates theme-provided scripts into Horde’s standard script pipeline (including caching/minification), while constraining script declarations to safe, theme-local *.js filenames.

Changes:

  • Extend Horde_Themes_Cache to read and validate $theme_scripts from a theme’s info.php.
  • Introduce Horde_Script_File_ThemeDir to resolve script filesystem paths/URLs against the theme directory.
  • Emit declared theme scripts from Horde_PageOutput::header() via a new _addThemeScripts() helper.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
lib/Horde/Themes/Cache.php Factors out theme info.php loading and adds validated $theme_scripts discovery via themeScripts().
lib/Horde/Script/File/ThemeDir.php New Horde_Script_File implementation for scripts located under a theme’s directory (themesfs/themesuri).
lib/Horde/PageOutput.php Adds _addThemeScripts() and calls it during header() so theme scripts are included through the normal script pipeline.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ralflang ralflang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When shipping this we also need add support to the modern src/ assets framework.

@ralflang
ralflang merged commit 973c42c into horde:FRAMEWORK_6_0 Aug 26, 2026
1 check passed
@ralflang

Copy link
Copy Markdown
Member

@pierrefardel would you mind testing #222 for the modern stack? Should be equivalent functionality.

ralflang added a commit that referenced this pull request Aug 30, 2026
ralflang added a commit that referenced this pull request Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants