mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
fix(chat): escape message text spliced into action-button handlers
A message containing an apostrophe followed by angle brackets — e.g. a bot
answering "Type 'channel <name>' for a description" — tore the action row
below it apart: the apostrophe closed the single-quoted onclick attribute,
the '<' ended the <button> tag, and the remainder ("...")' title="Quote">)
landed on the page as visible text, with reply and quote dead.
JSON.stringify() produced a valid JS literal but was spliced into markup
raw. jsArg() wraps it in escapeHtmlAttribute(), so quotes and brackets
survive as entities and the JS parser sees the literal only after the HTML
parser decodes the attribute. Applied to the quote/edit buttons in channel
chat and DM, and to the sender-name handlers (reply, map, block) that had
the same defect for a name containing an apostrophe.
Verified in a browser against the local container: all ten test messages
now render six clean buttons with no stray text, and quoting '<>' fills the
composer correctly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
93f171a1c1
commit
cbbbcbc8b2
4 files changed
+29
-6
No files matched your search
@@ -1677,7 +1677,7 @@ function createMessageElement(msg) {
|
||||
<i class="bi bi-clipboard-data"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn btn-outline-secondary btn-msg-action btn-msg-edit" onclick='resendMessage(${JSON.stringify(msg.content)})' title="${tHtml('chat.edit_title')}">
|
||||
<button class="btn btn-outline-secondary btn-msg-action btn-msg-edit" onclick='resendMessage(${jsArg(msg.content)})' title="${tHtml('chat.edit_title')}">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
${window.deviceCaps?.supports_raw_resend && typeof msg.id === 'number' ? `
|
||||
@@ -1710,14 +1710,14 @@ function createMessageElement(msg) {
|
||||
<div class="message-content">${processMessageContent(msg.content, { isOwn: false })}</div>
|
||||
${metaInfo ? `<div class="message-meta">${metaInfo}</div>` : ''}
|
||||
<div class="message-actions">
|
||||
<button class="btn btn-outline-secondary btn-msg-action" onclick="replyTo('${escapeHtml(msg.sender)}')" title="${tHtml('chat.msg.reply_title')}">
|
||||
<button class="btn btn-outline-secondary btn-msg-action" onclick="replyTo(${jsArg(msg.sender)})" title="${tHtml('chat.msg.reply_title')}">
|
||||
<i class="bi bi-reply"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-msg-action" onclick='quoteTo(${JSON.stringify(msg.sender)}, ${JSON.stringify(msg.content)})' title="${tHtml('chat.msg.quote_title')}">
|
||||
<button class="btn btn-outline-secondary btn-msg-action" onclick='quoteTo(${jsArg(msg.sender)}, ${jsArg(msg.content)})' title="${tHtml('chat.msg.quote_title')}">
|
||||
<i class="bi bi-quote"></i>
|
||||
</button>
|
||||
${contactsGeoCache[msg.sender] ? `
|
||||
<button class="btn btn-outline-secondary btn-msg-action" onclick="showContactOnMap('${escapeHtml(msg.sender)}', ${contactsGeoCache[msg.sender].lat}, ${contactsGeoCache[msg.sender].lon})" title="${tHtml('chat.msg.map_title')}">
|
||||
<button class="btn btn-outline-secondary btn-msg-action" onclick="showContactOnMap(${jsArg(msg.sender)}, ${contactsGeoCache[msg.sender].lat}, ${contactsGeoCache[msg.sender].lon})" title="${tHtml('chat.msg.map_title')}">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
@@ -1732,7 +1732,7 @@ function createMessageElement(msg) {
|
||||
</button>
|
||||
` : ''}
|
||||
${!isContactProtectedByName(msg.sender) ? `
|
||||
<button class="btn btn-outline-danger btn-msg-action" onclick="blockContactFromChat('${escapeHtml(msg.sender)}')" title="${tHtml('chat.msg.block_title', { name: msg.sender })}">
|
||||
<button class="btn btn-outline-danger btn-msg-action" onclick="blockContactFromChat(${jsArg(msg.sender)})" title="${tHtml('chat.msg.block_title', { name: msg.sender })}">
|
||||
<i class="bi bi-slash-circle"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
+1
-1
@@ -1499,7 +1499,7 @@ function displayMessages(messages) {
|
||||
// only raw resend (arrow-repeat).
|
||||
const resendBtn = msg.is_own ? `
|
||||
<div class="dm-actions">
|
||||
<button class="btn btn-outline-secondary btn-sm dm-action-btn" onclick='resendMessage(${JSON.stringify(msg.content)})' title="${tHtml('chat.edit_title')}">
|
||||
<button class="btn btn-outline-secondary btn-sm dm-action-btn" onclick='resendMessage(${jsArg(msg.content)})' title="${tHtml('chat.edit_title')}">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -309,6 +309,25 @@ function escapeHtmlAttribute(text) {
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a value as a JS literal safe to splice into an inline event
|
||||
* handler attribute.
|
||||
*
|
||||
* JSON.stringify() alone is not enough. Its output is dropped straight into
|
||||
* markup, so a message containing an apostrophe closes the attribute early and
|
||||
* a following '<' ends the whole tag — the rest of the button then leaks into
|
||||
* the page as visible text (e.g. `title="Quote">` next to a broken icon), and
|
||||
* the handler never runs. Escaping both quote characters and the angle
|
||||
* brackets keeps the literal intact for the JS parser, which sees the
|
||||
* attribute value only after HTML entities are decoded.
|
||||
*
|
||||
* @param {*} value - Value to pass to the handler
|
||||
* @returns {string} - Escaped JSON literal, ready for an attribute
|
||||
*/
|
||||
function jsArg(value) {
|
||||
return escapeHtmlAttribute(JSON.stringify(value === undefined ? null : value));
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
Share Cards — contacts, channels and positions shared inside chat messages
|
||||
=============================================================================
|
||||
|
||||
@@ -16,6 +16,10 @@ For deep technical notes, see [architecture.md](architecture.md). For the full g
|
||||
|
||||
- **Two ways to keep full access to your own demo.** Locking the instance is only half of it; you still need to administer the thing. `MC_DEMO_TRUSTED_NETS` takes a list of networks — `192.168.0.0/16`, say — whose visitors are never restricted, so from your own LAN the app behaves exactly as it always has, with nothing to type. `MC_DEMO_UNLOCK_CODE` sets a code you enter once under **Settings → Operator access**; the browser is then remembered for thirty days from any network, which is what you want when the instance is published through a tunnel and every visitor looks like it is coming from the same place. A *Lock again* button sits in the same spot for when you want to see the instance the way a visitor does. The cookie holds a derived token rather than the code itself, so changing the code in `.env` logs every unlocked browser out — which is also how you take access back. One warning worth repeating from the guide: if mc-webui is published through a tunnel or a reverse proxy, every visitor arrives from that one address, and if the tunnel runs on your LAN then listing your LAN as trusted would unlock the instance for the whole internet. mc-webui declines to make that mistake — an address that arrives with a proxy header is not matched against the trusted list unless `MC_TRUST_PROXY` is on — and says so in the log at startup, but the safe habit is to use the unlock code whenever the app is not reached directly.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **A message quoting something in angle brackets no longer breaks the row of buttons under it.** A bot answering with `Type 'channel <name>' for a description` was enough: the buttons beneath that message came out mangled, with a stray `title="Quote"` printed next to them, and the first two — reply and quote — stopped working. The cause was in how those buttons were built: the message text was pasted into them without being escaped, so an apostrophe closed the button early and a `<` ended it altogether, spilling the rest of the markup onto the page as text. The text is now escaped properly wherever it is handed to a button, in channel chat and in direct messages alike, so quotes, angle brackets and both kinds of quotation mark are just characters again. The same fix covers sender names containing an apostrophe, whose reply, map and block buttons were quietly dead for the same reason.
|
||||
|
||||
---
|
||||
|
||||
## 2.11.0 — 2026-08-20
|
||||
|
||||
Reference in new issue
Block a user