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.
This commit is contained in:
Joel Brock
2026-06-05 17:42:35 -07:00
parent b548b6425b
commit 4ca3c194d7
9 changed files with 457 additions and 58 deletions
+103
View File
@@ -0,0 +1,103 @@
# WebForm-mw — CiviCRM extension
Adds an **Engagement Report** tab to Organization contact-view pages that
embeds the FCI Co-op Survey staff report
(`https://survey.fci.coop/staff/report`) in an iframe. The tab shows the
report for the current organization, scoped by the contact id in the URL.
The embedded app handles its own auth via a shared staff secret. The
extension is otherwise read-only and adds no Civi tables, custom fields,
or scheduled jobs.
## Install
1. Copy the entire `webform-mw/` directory to your CiviCRM extensions
directory (typically `<civi-root>/sites/default/ext/` or wherever
`[civicrm.extensionsDir]` points in your `civicrm.settings.php`).
- The directory name on disk must be `webform-mw` (matching `<key>` in
`info.xml`).
2. In CiviCRM:
`Administer → System Settings → Extensions → Add new → Refresh`,
then click **Install** next to "WebForm-mw".
3. Configure (see next section). The tab will be hidden until both
settings are present.
## Configuration
The extension reads two values, in this order:
1. Constants in `civicrm.settings.php` (preferred):
```php
define('WEBFORM_MW_APP_URL', 'https://survey.fci.coop');
define('WEBFORM_MW_STAFF_KEY', '...the STAFF_REPORT_KEY shared with the app...');
```
2. Or environment variables (`WEBFORM_MW_APP_URL`, `WEBFORM_MW_STAFF_KEY`)
set wherever PHP-FPM / the web server reads its environment from.
`WEBFORM_MW_STAFF_KEY` must match the `STAFF_REPORT_KEY` configured on the
Next.js app (Amplify environment / SSM Parameter Store). The two are the
same shared secret; rotate them together.
`WEBFORM_MW_APP_URL` is the public base URL of the WebForm-mw deployment
(no trailing slash). Production: `https://survey.fci.coop`.
When either value is missing, the tab body shows a help banner with the
exact configuration snippet to paste, so anyone installing the extension
without prior context can self-serve.
## Behaviour
- Tab title: **Engagement Report**.
- Visible only on **Organization** contacts (Individuals and Households
see no tab). The check happens server-side in the tabset hook.
- Tab body is an iframe pointing at
`${WEBFORM_MW_APP_URL}/staff/report?org=<cid>&key=<secret>&frame=1`.
- `frame=1` tells the embedded app to suppress its site header/footer
and emit a `postMessage({type:"webform-mw-height", height})` payload
on render and on resize. The tab's small inline script listens for
this message and auto-sizes the iframe so there's no nested scrollbar.
- The script validates the postMessage `event.origin` against the
configured `WEBFORM_MW_APP_URL` origin before resizing.
## Security notes
- The staff secret travels with each tab render inside the iframe `src`.
Anyone permitted to view the Engagement Report tab (i.e., anyone with
CiviCRM access) can read the URL in their browser's DevTools and reuse
the secret to view any org's report. Acceptable model if "CiviCRM
access" and "should view any staff report" overlap; otherwise consider
upgrading the embedded app to per-user signed tokens and minting them
in the page controller.
- The extension uses `access CiviCRM` as its access argument — anyone
with that permission sees the tab on Organization contacts. Tighten by
changing the `<access_arguments>` value in `xml/Menu/webform_mw.xml`
to a more specific permission (e.g., `view all contacts`) and
reinstalling.
- The embedded app's CSP (`frame-ancestors`) must include the CiviCRM
origin or the iframe will refuse to render. The Next app reads
`CIVI_BASE_URL` at build time and adds its origin to the staff route's
CSP automatically. Confirm with `curl -I <app>/staff/report` after
deploy — you should see `Content-Security-Policy: ... frame-ancestors
'self' https://<your-civi-host> ...`.
## Files
- `info.xml` — extension manifest. Key `webform-mw`, type `module`.
- `webform_mw.php` — `hook_civicrm_tabset` + config readers.
- `CRM/WebformMw/Page/Tab.php` — page controller for the tab snippet.
- `templates/CRM/WebformMw/Page/Tab.tpl` — iframe + height-listener.
- `xml/Menu/webform_mw.xml` — registers the
`civicrm/contact/view/engagement-report` URL.
## Versions tested
- CiviCRM 5.50+
- PHP 7.4+ / 8.x
- Tested against Backdrop-as-host and Drupal-as-host CiviCRM deployments.
## Uninstalling
`Administer → Extensions → Disable`, then `Uninstall`. Removes nothing
from the database; the extension stores no Civi data of its own.