stores base64 text on disk. * - APIv3 Attachment.create + content:b64 -> same. * - APIv3 Attachment.create + multipart -> /civicrm/ajax/rest doesn't * expose $_FILES to the * action, so "file" is * silently ignored. * - APIv3 Attachment.create + options.move-file -> WORKS. Civi reads * the path, moves the file * into civicrm.files/upload, * writes correct bytes. * * Required POST fields: * file the binary (multipart `file` part) * entity_table e.g. "civicrm_contact" (per Attachment.create contract) * entity_id the entity id to link to * name (optional) file_name; defaults to the upload's name * mime_type (optional) defaults to the upload's reported type * * Returns JSON: { id, name } on success, { error } with 4xx/5xx otherwise. * * Authorization: `access CiviCRM`. AuthX is expected to authenticate the * Bearer + Site-Key headers WebForm-mw sends. */ class CRM_WebformMw_Page_Upload extends CRM_Core_Page { public function run() { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { $this->jsonError('POST required', 405); return; } if (!CRM_Core_Permission::check('access CiviCRM')) { $this->jsonError('Permission denied', 403); return; } if (empty($_FILES['file']) || !is_array($_FILES['file'])) { $this->jsonError('Missing file part', 400); return; } $upload = $_FILES['file']; if ((int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $this->jsonError('Upload failed (php error ' . (int) $upload['error'] . ')', 400); return; } if (empty($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name'])) { $this->jsonError('Invalid upload tmp path', 400); return; } $entityTable = (string) ($_POST['entity_table'] ?? ''); $entityId = (int) ($_POST['entity_id'] ?? 0); // Whitelist entity tables to mirror the redirect route's defensive scope. if (!in_array($entityTable, ['civicrm_contact', 'civicrm_activity'], TRUE)) { $this->jsonError('Invalid entity_table', 400); return; } if ($entityId <= 0) { $this->jsonError('Invalid entity_id', 400); return; } $name = (string) ($_POST['name'] ?? $upload['name'] ?? 'upload'); $mime = (string) ($_POST['mime_type'] ?? $upload['type'] ?? 'application/octet-stream'); try { $result = civicrm_api3('Attachment', 'create', [ 'entity_table' => $entityTable, 'entity_id' => $entityId, 'name' => $name, 'mime_type' => $mime, 'options' => [ 'move-file' => $upload['tmp_name'], ], ]); $fileId = NULL; if (!empty($result['id'])) { $fileId = (int) $result['id']; } elseif (!empty($result['values']) && is_array($result['values'])) { $first = reset($result['values']); if (!empty($first['id'])) { $fileId = (int) $first['id']; } } if (!$fileId) { throw new Exception('Attachment.create returned no id'); } $this->jsonOk(['id' => $fileId, 'name' => $name]); } catch (Throwable $e) { $this->jsonError('Attachment.create failed: ' . $e->getMessage(), 500); } } private function jsonOk(array $payload): void { header('Content-Type: application/json', TRUE, 200); echo json_encode($payload); CRM_Utils_System::civiExit(); } private function jsonError(string $message, int $status): void { header('Content-Type: application/json', TRUE, $status); echo json_encode(['error' => $message]); CRM_Utils_System::civiExit(); } }