Files
WebForm-mw/civi-extension/webform-mw
Joel Brock 325615576a Upload: route through Civi extension multipart endpoint
Every JSON-based upload path on this Civi stores the `content` field
verbatim on disk — confirmed against both APIv4 File.create AND APIv3
Attachment.create (both came back as base64 text in hex dumps). The
multipart `file` part to /civicrm/ajax/rest is also a dead end: APIv3
Attachment.create on this install doesn't see $_FILES (rejected with
"Mandatory key(s) missing: id or content or options.move-file").

The one path Civi honors is APIv3 Attachment.create + options.move-file
— pointing at a filesystem path the Civi server can read. So expose a
tiny multipart endpoint in the WebForm-mw Civi extension that copies
PHP's $_FILES['file']['tmp_name'] into the API call, then return the
new file id as JSON. PHP's $_FILES preserves binary natively.

Civi extension (requires admin deploy):
- CRM/WebformMw/Page/Upload.php  : multipart POST handler. Validates
  the upload, requires `access CiviCRM`, whitelists entity_table to
  civicrm_contact|civicrm_activity, calls Attachment.create with
  move-file pointing at the tmp upload, returns {id, name} JSON.
- xml/Menu/webform_mw.xml        : registers civicrm/webform-mw/upload.

WebForm-mw side:
- lib/civicrm.ts : new civiMultipart() helper. POSTs multipart to an
  arbitrary Civi path (not /civicrm/ajax/rest) with the same AuthX
  headers. Returns the parsed JSON body.
- app/api/upload/route.ts : send the upload's bytes via civiMultipart
  to civicrm/webform-mw/upload. Comment-block now records all four
  upload paths we tried so a future reader doesn't repeat the cycle.

Deploy: admin syncs the updated civi-extension/webform-mw/ directory
and Disable/Re-enables the extension (or runs cv flush) so the new
menu route is registered.
2026-06-10 12:28:43 -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>&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.phphook_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.