` 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); } }