diff --git a/civi-extension/webform-mw/CRM/WebformMw/Page/File.php b/civi-extension/webform-mw/CRM/WebformMw/Page/File.php new file mode 100644 index 0000000..fdc6421 --- /dev/null +++ b/civi-extension/webform-mw/CRM/WebformMw/Page/File.php @@ -0,0 +1,65 @@ +` and rely on us to mint the `fcs` JWT that + * Civi's `/civicrm/file` handler requires. The JWT is signed with the + * site's crypto key, which we have here because we're running inside + * CiviCRM; the Next.js side doesn't. + * + * Permission: `access CiviCRM`. The user is already authenticated in the + * parent Civi tab when they click a link in the iframe; their session + * cookie travels with the new-tab navigation. + * + * URL shape: + * /civicrm/webform-mw/file?id=[&eid=] + * + * Behavior: + * - Resolve `eid` from civicrm_entity_file if not provided. + * - Mint a short-lived JWT with payload {exp, civi.file: } + * matching Civi's own /civicrm/file token format. + * - 302-redirect to /civicrm/file?reset=1&id=...&eid=...&fcs=. + */ +class CRM_WebformMw_Page_File extends CRM_Core_Page { + + public function run() { + $fileId = (int) CRM_Utils_Request::retrieve('id', 'Positive', $this, TRUE); + $eid = (int) CRM_Utils_Request::retrieve('eid', 'Positive', $this, FALSE, 0); + + // Resolve eid from the entity_file join if the caller didn't supply + // one. Any linked entity works for URL-fingerprint purposes; the JWT + // we mint below is what Civi actually authenticates on. + if (!$eid) { + $dao = CRM_Core_DAO::executeQuery( + "SELECT entity_id FROM civicrm_entity_file WHERE file_id = %1 LIMIT 1", + [1 => [$fileId, 'Positive']] + ); + if ($dao->fetch()) { + $eid = (int) $dao->entity_id; + } + } + + // Mint the fcs JWT. Payload matches the structure Civi's own file + // URL builder emits: {exp, "civi.file": ""}. One-week lifetime — + // these links are typically clicked seconds after the report renders, + // but the staff report can be left open for a while in a Civi tab. + $payload = [ + 'exp' => time() + 60 * 60 * 24 * 7, + 'civi.file' => (string) $fileId, + ]; + $fcs = \Civi::service('crypto.jwt')->encode($payload); + + $url = CRM_Utils_System::url( + 'civicrm/file', + "reset=1&id={$fileId}&eid={$eid}&fcs=" . urlencode($fcs), + FALSE, + NULL, + FALSE, + TRUE + ); + CRM_Utils_System::redirect($url); + } + +} diff --git a/civi-extension/webform-mw/xml/Menu/webform_mw.xml b/civi-extension/webform-mw/xml/Menu/webform_mw.xml index a9c1494..1c0444a 100644 --- a/civi-extension/webform-mw/xml/Menu/webform_mw.xml +++ b/civi-extension/webform-mw/xml/Menu/webform_mw.xml @@ -6,4 +6,10 @@ CRM_WebformMw_Page_Tab access CiviCRM + + civicrm/webform-mw/file + WebForm-mw file redirect + CRM_WebformMw_Page_File + access CiviCRM + diff --git a/components/StaffReportView.tsx b/components/StaffReportView.tsx index 037110c..dae58a0 100644 --- a/components/StaffReportView.tsx +++ b/components/StaffReportView.tsx @@ -460,16 +460,17 @@ function FieldValue({ const id = String(v.id); const name = v.file_name ?? `file-${id}`; // Prefer the Civi-signed URL (carries the fcs JWT) returned by - // Attachment.get; Civi's file handler crashes on a null fcs decode if we - // hit /civicrm/file?id=X bare. Fall back to a bare URL only if signed - // URLs weren't available (e.g. older Civi without `url` on Attachment). + // Attachment.get. If absent, fall back to the WebForm-mw Civi extension's + // file-redirect route — it mints the fcs server-side and 302s to the + // real /civicrm/file URL. (Hitting /civicrm/file?id=X bare crashes Civi + // on a null fcs JWT decode.) let href = "#"; if (v.url) { href = v.url.startsWith("http") ? v.url : `${civiBaseUrl}${v.url.startsWith("/") ? "" : "/"}${v.url}`; } else if (civiBaseUrl) { - href = `${civiBaseUrl}/civicrm/file?reset=1&id=${encodeURIComponent(id)}`; + href = `${civiBaseUrl}/civicrm/webform-mw/file?id=${encodeURIComponent(id)}`; } return (