Skip to content

fix(view): pass standard Underscore templateSettings on every compile#404

Merged
bjagg merged 1 commit into
uPortal-Project:masterfrom
bjagg:fix/template-sigil-leak
May 5, 2026
Merged

fix(view): pass standard Underscore templateSettings on every compile#404
bjagg merged 1 commit into
uPortal-Project:masterfrom
bjagg:fix/template-sigil-leak

Conversation

@bjagg

@bjagg bjagg commented May 5, 2026

Copy link
Copy Markdown
Member

Problem

Every Underscore template in CalendarPortlet (event list, event detail, admin role-param row) currently renders its raw source into the portlet body instead of being interpolated. The events column shows literal text like:

<% if (_(days).size() === 0) { %>
    ...
    <%= day.displayName %>
    ...
    <%= event.attributes.summary %>

…where the actual events should appear. Screenshot before:

| ... small calendar grid ...  | <% if (_(days)... %>... raw template text ... |

Root cause

uPortal's respondr.xsl (uPortal-webapp/src/main/resources/layout/theme/respondr/respondr.xsl, ~line 274) does:

up._ = _.noConflict();
up._.templateSettings = {
    interpolate: /{{=(.+?)}}/g,
    evaluate:    /{{(.+?)}}/g,
    escape:      /{{-([\s\S]+?)}}/g
};

scripts.jsp aliases that same instance via ${n}._ = up._, so every _.template(...) call in this portlet inherits the Mustache delimiters at runtime. The portlet's templates use the standard <% %> / <%= %> sigils, which no longer match anything in the (Mustache) regex. Underscore returns the source unchanged, the rendered HTML is the template, and the user sees the raw Underscore syntax in the events column.

The Mustache override in uPortal is intentional and shipping: Marketplace and other portlets rely on it. Mutating up._.templateSettings back from this portlet would break those.

Fix

Set a per-portlet-namespace ${n}.upcalTemplateSettings object once in scripts.jsp and pass it to every _.template(...) call as the second argument. Underscore 1.8.3's _.template(text, settings) does _.defaults({}, settings, _.templateSettings), so the explicit settings win without touching the shared global.

The regex literals in scripts.jsp are emitted via the ${\"<%\"}...${\"%>\"} EL trick — the same trick the view JSPs already use to write literal Underscore sigils into rendered HTML templates — so JSP's own scriptlet parser doesn't swallow the regex while compiling the page.

Files changed

  • scripts.jsp — define ${n}.upcalTemplateSettings.
  • calendarWideView.jsp — pass settings to both _.template(...) calls (event list, event detail).
  • calendarNarrowView.jsp — same two calls.
  • editCalendarDefinition.jsp — pass settings to the role-param row template (the JSP that errored at the screenshot below isn't this one, but the fix is uniform).

Verification

uPortal-start Playwright spec tests/ux/portlets/calendar.spec.ts gets a new regression guard: "event list renders without leaking Underscore template sigils". Asserts the .upcal-event-list does not contain the substring <%. Fails before this PR, passes after redeploying CalendarPortlet.

Visual confirmation: the events column now shows the expected "No events" alert (or the populated day list) instead of raw template text.

Notes

  • The same root cause was the bug — every site emits <% %> regex literals through the same EL escape (${\"<%\"}/${\"%>\"}) precisely because JSP cannot tolerate raw <% in the source. I made the same mistake mid-fix and got JspException immediately, which is how I traced the EL-escape pattern back into scripts.jsp.
  • I also considered putting the settings object in calendar.js (upcal.tplSettings) so the JSPs would pull it from the compiled JS bundle. Decided against — scripts.jsp already runs per-portlet-instance and namespaces under ${n}, which keeps the override scoped without leaking into other portlets that load this WAR.

Problem: every Underscore template in this portlet (event list, event
detail, role-param row) currently renders its raw source into the
portlet body instead of being interpolated. The events column shows
literal `<% if (_(days).size() === 0) { %>...<%= day.displayName %>...`
text where the actual events should appear.

Why: uPortal's respondr.xsl global init (in uPortal core,
respondr.xsl ~line 274) calls `up._ = _.noConflict()` and then
overwrites the shared `up._.templateSettings` with Mustache-style
delimiters:

    interpolate: /{{=(.+?)}}/g
    evaluate:    /{{(.+?)}}/g
    escape:      /{{-([\s\S]+?)}}/g

scripts.jsp aliases that same instance via `${n}._ = up._`, so every
`_.template(...)` call in this portlet inherits the Mustache
delimiters. The portlet's templates use the standard `<% %>` /
`<%= %>` sigils, which no longer match anything in the regex, and
Underscore returns the source unchanged.

Goal: have the portlet's templates compile against the delimiters
they were authored for, without mutating the shared `up._` (which
would break other portlets — Marketplace, etc. — that already rely
on the Mustache override).

Changes:
- scripts.jsp: define a per-portlet-namespace
  `${n}.upcalTemplateSettings` object holding the standard sigils.
  The regex literals must be emitted as `${"<%"}...${"%>"}` so JSP's
  own scriptlet parser doesn't swallow them — same trick the view
  JSPs already use to write the literal sigils into the rendered
  HTML templates.
- calendarWideView.jsp, calendarNarrowView.jsp,
  editCalendarDefinition.jsp: pass `${n}.upcalTemplateSettings` as
  the second argument to every `_.template(...)` call so the
  per-call settings shadow the global Mustache defaults without
  touching them.

Notes: Underscore 1.8.3's `_.template(text, settings)` does
`_.defaults({}, settings, _.templateSettings)`, so the explicit
settings win cleanly. Verified end-to-end with Playwright in
uPortal-start: a regression guard
(`tests/ux/portlets/calendar.spec.ts › event list renders without
leaking Underscore template sigils`) goes from failing to passing
once this change is deployed.
@bjagg bjagg merged commit 3ea3012 into uPortal-Project:master May 5, 2026
5 checks passed
bjagg added a commit to bjagg/uPortal-start that referenced this pull request May 5, 2026
Problem: this branch has been carrying tests and overlay seeds that
depend on three companion fixes published over the last few days
(uPortal Bootstrap dedupe, CalendarPortlet template-sigil leak,
NotificationPortlet jjwt runtime classpath). Until those landed on
Maven Central, CI couldn't go green here even though every spec
passed locally against the locally-built SNAPSHOTs.

Goal: consume the published GA versions so PR uPortal-Project#692 lights up green
and is ready to merge.

Changes:
- uPortalVersion 5.17.4 → 5.17.5 (picks up
  uPortal-Project/uPortal#2980 — Bootstrap 5 include de-dup in the
  respondr skin, fixes silently-broken Options menus / favorites /
  rate-portlet across the dashboard)
- calendarPortletVersion 2.7.2 → 2.7.3 (picks up
  uPortal-Project/CalendarPortlet#404 — Underscore template
  settings override per call, fixes the events column rendering raw
  template source instead of event entries)
- notificationPortletVersion 4.8.3 → 4.8.4 (picks up
  uPortal-Project/NotificationPortlet#695 — pin jjwt-impl +
  jjwt-jackson at runtime, fixes /api/v2 silently 403'ing and the
  notification web components rendering empty)

Notes: locally redeployed the three webapps from Maven Central
artifacts and ran the Playwright suite three times in a row —
117/117 each pass — to confirm the published versions match what we
were testing against the SNAPSHOTs.
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.

1 participant