The Engagement Report tab wrote STAFF_REPORT_KEY into the iframe src for
every user with "access CiviCRM", without checking contact permissions.
Anyone could copy it from the DOM and read any org's report and files,
bypassing Civi ACLs; the key never expired and landed in access logs.
- Extension (0.4.0): Tab.php checks Contact_Permission::allow(VIEW) and
mints sig = base64url(HMAC-SHA256(key, "webform-mw.staff-report.v1:
<org>:<exp>")) with an 8h exp. The secret no longer leaves the server.
- App: lib/staff-token.mjs verifies the token (constant-time, org must
match, rejects expired or >12h tokens). /staff/report, /api/staff/report
and /api/staff/file share staffAuthQuery(); client links carry exp/sig.
- Legacy ?key= is still accepted (constant-time compare, logs a warning)
so the app can deploy before the extension. Remove it once every Civi
install runs extension 0.4.0.
- PHP/Node signature compatibility is pinned by a PHP-generated vector.
The staff-report iframe is expanded to full content height (no nested
scrollbar), so a modal <dialog> centered in its viewport landed in the
middle of the whole document and sat off-screen unless the parent page
was scrolled there. Cross-origin means the iframe can't read the
parent's scroll position itself.
Tab.tpl now broadcasts the iframe's visible slice (webform-mw-viewport,
rAF-throttled on scroll/resize/load and after each height change), and
AttachmentLightbox pins to that slice and fills its height. Falls back
to a fixed box when no viewport message arrives (older extension), and
standalone mode keeps native centering at a taller 85vh.
Extension bumped to 0.3.1 (template change -> re-copy + cv flush on
prod). PRODUCTION_CUTOVER.md change log updated.
Three quick fixes off first-deploy testing:
1. /api/staff/file 404'd for valid files. Refactor fileBelongsToOrg
to SELECT the org's and activities' file columns and JS-compare
instead of WHERE ... OR with custom field refs (APIv4 fragility
around nested OR + dotted custom fields). Same ownership probe,
same shape /api/staff/report itself uses to read file values.
2. Lightbox ballooned to full report height because the staff iframe
auto-grows to fit content (often 3000+ px). Cap to a fixed
640px x min(92vw, 900px) box so it stays a reasonable preview
regardless of iframe document size.
3. Production frame-ancestors blocked crm.fci.coop from iframing
survey.fci.coop -- the CSP only included the dev Civi origin
derived from CIVI_BASE_URL. Add CIVI_FRAME_ALLOWED_ORIGINS
(comma-separated) so one app deploy can be embedded by both
dev and prod Civi. Falls back to CIVI_BASE_URL for single-Civi
compatibility.
PRODUCTION_CUTOVER.md updated inline and in the change log.
Adds a /api/staff/file proxy that re-streams Civi attachments with
Content-Disposition: inline so a native <dialog> lightbox can preview
images and PDFs in place. Office docs keep their plain download link
and gain a "View in Google Docs" secondary link (uses the Civi-signed
URL so Google can fetch without our staff key).
Also threads mime through /api/staff/report (Attachment.get mime_type)
so the dispatcher picks the right affordance without relying solely on
filename inference.
Each stage-transition activity now becomes a horizontal range on its lane: start = the activity's date, end = the next activity with a higher stage rank, or extending to today if still in effect. Milestone date-field dots overlay on top of the ranges. Activity dots at each range start carry a tooltip with the activity subject. Added computeStageRanges in lib/stageRank.ts and switched DateTimeline to take activities directly (deriving both the rank resolver and the ranges internally). Wide year-spanning timelines now show their full extent even when no milestone dates have been entered.
Each date-field event is now plotted on the lane corresponding to the Framework Stage the co-op was in on the field's stored date, resolved from the activity stream's stage transitions. Date_Opened is pinned to Stage 5 as the journey anchor. Events that pre-date any known transition fall back to the field's section rank so they still surface somewhere. Extracts the STAGE_RANK map (previously inline in ReportView) into lib/stageRank.ts alongside the new buildStageRankAtDate resolver factory.
DateTimeline now skips section rank 0 (intake) and renders stage lanes
top-to-bottom 5→1, matching how the framework actually numbers stages.
MembershipChart's latest-Actual label was placed at the circle's
x + 6, which overflowed the SVG when the most-recent point landed near
the right padding. Anchor it to xOf(maxT) - 4 with textAnchor="end",
mirroring the Goal label so both endpoint labels stay inside the chart.