diff --git a/frontend/lib/CPIM.ts b/frontend/lib/CPIM.ts index 74667de..34eaa9e 100644 --- a/frontend/lib/CPIM.ts +++ b/frontend/lib/CPIM.ts @@ -21,7 +21,8 @@ export class CPIM { static fromString(raw: string): CPIM { let cpim = new CPIM(); - let parts = raw.split("\n\n"); + // CRLF normalization — Linphone-emitted CPIM uses CRLF per RFC 3862; mirrors PHP-side fix in src/CPIM.php + let parts = raw.replace(/\r\n/g, "\n").split("\n\n"); cpim.addHeaders(parts[0]); cpim.addHeaders(parts[1]); diff --git a/lua/index.lua b/lua/index.lua index 512e6c2..8543824 100644 --- a/lua/index.lua +++ b/lua/index.lua @@ -9,7 +9,12 @@ function uriescape (s) return '%'..string.format("%02X", string.byte(c)); end ); - s = string.gsub(s, "%s", "+"); + -- Encode whitespace as %20 (RFC 3986), not "+" (form-urlencoded). Together with the + -- first gsub above (which escapes literal "+" to %2B), this gives a fully RFC 3986– + -- consistent encoding. mod_curl decodes %XX in transit but leaves "+" literal, so the + -- receiving side (outbound-hook.php) must use rawurldecode (RFC 3986) to match. + -- Do NOT change to "+" without coordinating the decoder change in outbound-hook.php. + s = string.gsub(s, "%s", "%%20"); return s; end diff --git a/outbound-hook.php b/outbound-hook.php index adb6c63..e0ac997 100644 --- a/outbound-hook.php +++ b/outbound-hook.php @@ -25,19 +25,35 @@ // ── Normalize fields based on payload source ── // Web UI sends extensionUUID (camelCase); FS event has extension_uuid (set by chatplan user_data) +// +// rawurldecode (RFC 3986) used on both branches: +// +// - FS-event branch: paired with lua/index.lua's `uriescape`, which encodes literal `+` as +// `%2B` (step 1) and whitespace as `%20` (step 2 — see Lua comment there). mod_curl +// decodes `%XX` in transit but leaves `+` literal. rawurldecode here is the matching +// decoder; effectively a no-op for properly-encoded bodies, defensive against residue. +// IMPORTANT: do NOT switch to urldecode without also reverting the Lua line-12 fix — +// the encoder and decoder must be consistent (RFC 3986 throughout). Mismatched +// urldecode form-urlencoded behavior would mangle literal `+` to space. +// +// - Web UI branch: frontend sends raw JSON (no `encodeURIComponent`/`encodeURI`), so +// $event->body and $event->id arrive as raw values from json_decode. rawurldecode is +// a no-op for these; chosen for consistency and to incidentally fix a latent +// `+`-corruption bug where urldecode would have mangled user-entered literal `+`. + if (isset($event->extensionUUID)) { // Web UI payload (Ian's webtexting) $to = $event->to; $contentType = $event->contentType; - $body = urldecode($event->body); - $dedupeID = urldecode($event->id); + $body = rawurldecode($event->body); + $dedupeID = rawurldecode($event->id); $extensionUUID = $event->extensionUUID; } else { // FreeSWITCH event payload (from chatplan Lua via mod_curl) // extension_uuid set by chatplan: ${user_data(${from_user}@${from_host} var extension_uuid)} $to = $event->to_user; $contentType = isset($event->type) ? $event->type : 'text/plain'; - $body = isset($event->_body) ? urldecode($event->_body) : ''; + $body = isset($event->_body) ? rawurldecode($event->_body) : ''; $dedupeID = isset($event->{'sip_h_X-Message-ID'}) ? $event->{'sip_h_X-Message-ID'} : uuid(); $extensionUUID = $event->extension_uuid; } diff --git a/threadlist.php b/threadlist.php index 2f9b314..e9a4e5b 100644 --- a/threadlist.php +++ b/threadlist.php @@ -256,7 +256,7 @@ class='fas fa-check fa-fw'>Start - +