Files
Joel Brock 0a19b57a86 Harden the extension upload endpoint (CSRF, record ACL, mime allowlist)
civicrm/webform-mw/upload accepted any session-authenticated POST, called
civicrm_api3('Attachment','create') (which skips permission checks from
PHP) against any contact/activity id, and stored any client mime type.

- Require X-Requested-With: XMLHttpRequest (already sent by the app), so
  cross-site form posts can't ride a logged-in staff session.
- Require EDIT permission on the target contact/activity.
- Enforce the same mime allowlist as the app's /api/upload route.
2026-10-30 11:18:10 -07:00
..

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):

    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>&exp=<unix>&sig=<hmac>&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 shared secret never leaves the server. Each tab render mints a signed token, HMAC-SHA256(secret, "webform-mw.staff-report.v1:<org>:<exp>"), valid for one org for 8 hours. The app rejects tokens for other orgs, expired tokens, and anything valid for longer than 12 hours.
  • Tokens are only minted after CRM_Contact_BAO_Contact_Permission::allow() confirms the current user can view the org, so Civi contact ACLs carry through to the embedded report.
  • Rotating the secret: change WEBFORM_MW_STAFF_KEY and the app's STAFF_REPORT_KEY together; outstanding tokens stop working at once.
  • 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.