Commit Graph
6 Commits
Author SHA1 Message Date
Joel Brock 2e1244aa47 Staff report: hide section anchor nav when framed in Civi
The iframe in the Engagement Report tab is sized to fit content, so it
has no internal scroll context. Clicking an anchor link inside it
changes the URL hash but the iframe content doesn't move and the user
has to scroll the outer CiviCRM page manually. Rather than coordinate
cross-frame scroll with the parent, just skip rendering the anchor
strip when framed. Standalone view is unchanged.
2026-06-09 17:22:27 -07:00
Joel Brock 38f4738eca Staff report: fix infinite iframe-grow loop when framed in Civi
The framed report posts its content height to the parent so the Civi tab
can resize the iframe to fit. Two pieces interacted badly:

  - The root layout sets html.h-full and body.min-h-full, so documentElement
    and body heights track the iframe's viewport height.
  - The parent template sets iframe.height = postedHeight + 24 every time
    a height message arrives.

The combination produced an unbounded feedback loop: parent grows the
iframe by 24px, viewport grows, document height grows, ResizeObserver
fires, we post the new height, parent grows by another 24px. The outer
CiviCRM page scrollbar visibly shrank each cycle.

Fix on the report side (no extension change needed): when framed, override
html height to auto and body min-height to 0 so the document decouples
from the viewport. Observe body (the actual content), measure
body.scrollHeight, and skip posting when the value is unchanged. Original
styles are restored on unmount so route changes back to the standalone
view still work.
2026-06-09 17:14:54 -07:00
Joel Brock 4ca3c194d7 Staff report: CSP frame-ancestors + frame-mode + WebForm-mw Civi extension
App side:
- Per-route CSP: /staff/report now sets frame-ancestors 'self'
  <CIVI_BASE_URL origin> and drops X-Frame-Options so the CiviCRM
  extension can iframe it. All other routes keep frame-ancestors
  'none' + X-Frame-Options: DENY via a path-negation source.
- Staff page recognises ?frame=1 and renders without SiteHeader/
  SiteFooter so it fills the iframe cleanly.
- StaffReportView posts its scrollHeight to the parent window via
  postMessage when framed; the Civi tab listens and auto-resizes
  the iframe (no nested scrollbar). Anchor strip drops its sticky
  positioning in frame mode since there's no internal scroll.

CiviCRM extension (civi-extension/webform-mw/, key webform-mw):
- info.xml + main hook file (webform_mw.php) implementing
  hook_civicrm_tabset to add an 'Engagement Report' tab to
  Organization contact-view pages.
- CRM/WebformMw/Page/Tab.php + Smarty template render an iframe
  pointing at <WEBFORM_MW_APP_URL>/staff/report?org=<cid>&key=&frame=1,
  with a postMessage listener that validates event.origin against
  the configured app URL before resizing.
- Config via PHP constants in civicrm.settings.php (WEBFORM_MW_APP_URL,
  WEBFORM_MW_STAFF_KEY) or matching env vars. Help banner shown when
  unconfigured.
- README documents install, config, behaviour, security caveats.
2026-06-05 17:42:35 -07:00
Joel Brock b548b6425b Staff report: compact rows, anchor nav, Civi file links, Y1 matrix
UX iteration after first live look:
- Sticky anchor strip below the header with a chip per section (incl.
  Submissions) so staff can jump around a long page.
- Compact one-line rows that show only the latest value; multi-history
  fields get a muted 'N earlier entries' toggle that reveals the rest
  inline. Same affordance for file fields.
- Empty fields collapse under a single 'N empty fields' toggle per
  section instead of taking a row each.
- Stage 5: Y1_Q<n>_<metric> fields render as a read-only matrix table
  (rows: metrics; columns: Q1..Q4) matching the form's matrix layout.

File proxy (/api/staff/file) deleted. APIv4 Attachment isn't exposed
on this Civi instance (per the June upload spike), which is why the
previous proxy returned broken images. Staff are already authenticated
to Civi when they arrive here, so file fields now render as outbound
links to CIVI_BASE_URL/civicrm/file?reset=1&id=<id> and the browser
uses the staff session. No more proxy auth, no more SSRF surface to
harden, no broken images.

CIVI_BASE_URL flows from the staff page (server component) into the
client as a prop. No secret material crosses the boundary.
2026-06-05 17:28:17 -07:00
Joel Brock d7a1396640 Staff file proxy: harden against SVG XSS and SSRF
- Allowlist inline MIME types (png/jpeg/gif/webp/pdf only); everything
  else, including SVG and HTML, served as application/octet-stream
  with content-disposition: attachment.
- X-Content-Type-Options: nosniff and a restrictive CSP on every response.
- Validate the upstream URL Civi returns: must match CIVI_BASE_URL origin
  before we attach basic-auth creds and follow it. redirect: manual to
  prevent off-host hops.
- Drop SVG from the client's inline-image list (server forces download).
2026-06-05 17:02:42 -07:00
Joel Brock f889212296 Staff report: StaffReportView client component 2026-06-05 16:35:15 -07:00