Copy share link silently fails on IPv6 / non-secure HTTP origins #99
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#99
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bug
Clicking Copy on either share link in the share modal does nothing when the whiteboard is opened over an IPv6 address (e.g.
http://[4c0:b29a:2940:5d2:1::1]:9988/...). No clipboard write, no feedback toast, no error visible to the user.Root cause
crates/hero_whiteboard_ui/templates/web/home.html:782-790callsnavigator.clipboard.writeText(url)and chains only a.then()— no.catch().The Clipboard API is only exposed in secure contexts (HTTPS, plus the loopback exceptions
localhost,127.0.0.1, and[::1]). Any other IPv6 address served over plain HTTP is not a secure context, sonavigator.clipboardisundefinedand.writeText(...)throws synchronously. With no.catch()the failure is silent — the user clicks Copy and nothing happens.Expected
The Copy button works on any reachable origin (HTTP or HTTPS, IPv4 or IPv6) or, at minimum, surfaces a visible error so the user knows to copy manually.
Fix
In
home.html::copyLink(url):navigator.clipboardandwriteTextare unavailable, and fall back to adocument.execCommand("copy")flow via a temporary<textarea>(selected + copied + removed)..catch()to the clipboard promise that, on rejection, also runs the fallback.The input fields in the share modal are already
readonlyinputs, so users can also select/copy them directly — but the Copy button should still work.Acceptance
localhost/IPv4/HTTPS (regression check).Notes
UI-only change. No server, SDK, or DB changes. The same fallback should be considered anywhere else
navigator.clipboardis used (search for usages before fixing).Implementation Spec for Issue #99
Objective
Make every "Copy link" / "Copy" affordance in the Hero Whiteboard UI work on any reachable origin, including plain-HTTP IPv6 (and any other non-secure context where
navigator.clipboardisundefined). When the modern Clipboard API is missing or rejects, fall back to adocument.execCommand('copy')flow over a temporary<textarea>. If both paths fail, surface a distinct error toast so the user knows to copy the link manually instead of silently doing nothing.Requirements
navigator.clipboard/navigator.clipboard.writeTextand route to a synchronousdocument.execCommand('copy')fallback that creates an off-screen<textarea>, selects it, copies, and removes it.navigator.clipboard.writeText(...)in a.then(success).catch(...)chain; on rejection, run the sameexecCommandfallback.execCommand('copy')returnsfalse(or throws), show an error toast in the existing error style (red#dc3545, distinct from green#22c55e"Copied!") with text "Copy failed — select the link and copy manually."navigator.clipboardusage in the repo, not just the share modal inhome.html.Files to Modify
crates/hero_whiteboard_ui/templates/web/home.html— rewritecopyLink(url)(around line 782) to use a unified copy helper with fallback + error toast.crates/hero_whiteboard_ui/static/web/js/whiteboard/app.js— add a new exportedcopyToClipboard(text, successMsg)onWhiteboardAppthat performs Clipboard API →execCommandfallback → red error toast (using the existingshowToast(msg, isError)).crates/hero_whiteboard_ui/static/web/js/whiteboard/ui-helpers.js— replace the two inlineonclick="navigator.clipboard.writeText(...);WhiteboardApp.showToast('Copied!')"strings with calls toWhiteboardApp.copyToClipboard(...).crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js— replace the barenavigator.clipboard.writeText(url); WhiteboardApp.showToast('Link copied!');withWhiteboardApp.copyToClipboard(url, 'Link copied!').crates/hero_whiteboard_ui/templates/web/board.html— replace the two inlineonclick="navigator.clipboard.writeText(...);WhiteboardApp.showToast('Copied!')"strings with calls toWhiteboardApp.copyToClipboard(...).Implementation Plan
Step 1: Add a centralized clipboard helper to
WhiteboardAppFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/app.jsshowToast, addfunction copyToClipboard(text, successMsg)(defaultsuccessMsg = 'Copied!').function fallbackCopy(text)that creates a<textarea>, setsvalue, applies styles to keep it off-screen and non-interactive (position:fixed;top:-9999px;left:-9999px;opacity:0;), setsreadonly/aria-hidden, appends todocument.body, calls.focus()+.select()+setSelectionRange(0, text.length), wrapsdocument.execCommand('copy')intry/catch, removes the textarea, returns the boolean result (false on throw or false return).onSuccess()(→showToast(successMsg, false)) andonFailure()(→showToast('Copy failed — select the link and copy manually.', true)).navigator.clipboard && typeof navigator.clipboard.writeText === 'function', callnavigator.clipboard.writeText(text).then(onSuccess).catch(function() { if (fallbackCopy(text)) onSuccess(); else onFailure(); });.fallbackCopy(text)synchronously and toast accordingly.copyToClipboardon the same returned object that already exposesshowToast.Step 2: Wire all module-level call sites to the new helper
Files:
crates/hero_whiteboard_ui/static/web/js/whiteboard/ui-helpers.js,crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js,crates/hero_whiteboard_ui/templates/web/board.htmlui-helpers.jsline 39 and line 44: replaceonclick="navigator.clipboard.writeText(document.getElementById('share-view-url').value);WhiteboardApp.showToast('Copied!')"(and theshare-edit-urlvariant) withonclick="WhiteboardApp.copyToClipboard(document.getElementById('share-view-url').value)".board.htmllines 391 and 396: same two replacements (these strings are duplicated from ui-helpers.js).properties.jsline 992: replacenavigator.clipboard.writeText(url); WhiteboardApp.showToast('Link copied!');withWhiteboardApp.copyToClipboard(url, 'Link copied!');(remove the now-redundant explicit toast call).WhiteboardApp).Step 3: Replace
copyLinkin the home page templateFiles:
crates/hero_whiteboard_ui/templates/web/home.htmlWhiteboardApp. Inline equivalent logic locally.copyLink(url)(lines 782–791) so it:showCopyToast(message, isError)modeled on the current inline toast but parameterized:background = isError ? '#dc3545' : '#22c55e'. Keeps the existing positioning, animation, and 1500 ms timeout.fallbackCopy(text)identical to Step 1's (off-screen textarea +execCommand('copy')returning a boolean).navigator.clipboard?.writeTextexists, call it and chain.then(→ success toast).catch(→ fallback then success or error toast).fallbackCopy(url)synchronously and toast accordingly.escapeAttrand callcopyLink('...'), so no changes are needed there —copyLinkkeeps the same signature.WhiteboardApp).Acceptance Criteria
http://[2001:db8::1]/...), clicking any "Copy" / "Copy link" button copies the link via the textarea fallback and shows the green "Copied!" toast.localhost,127.0.0.1,[::1]), the existing Clipboard API path still runs and shows the green "Copied!" toast — no regression.execCommand('copy')fail, a red error toast appears with text "Copy failed — select the link and copy manually." (distinct from the green success toast).navigator.clipboardreference is left without a.catch()andexecCommandfallback. Confirmed bygrep -rn "navigator.clipboard" crates/hero_whiteboard_ui/.home.html:783,properties.js:992,board.html:391,board.html:396,ui-helpers.js:39,ui-helpers.js:44.copyLink(url)inhome.htmlkeeps its public signature so the existingonclickhandlers continue to work without template changes.Notes
document.execCommand('copy')is a built-in browser API and remains supported by every browser that runs the whiteboard UI today; it is deprecated but remains the canonical fallback for non-secure contexts.escapeAttrhelper used to embed URLs intoonclick="copyLink('...')"only escapes'and". The newcopyLinkbody does not introduce any additional injection risk because the URL value is only forwarded to clipboard APIs and totextarea.value, never re-injected as HTML.document.body(not the share modal) so it is not removed by modal close handlers mid-copy. It should setreadonlyandaria-hidden="true"to avoid accidental keyboard focus.setSelectionRange(0, text.length)after.select()for the textarea fallback to work — included in the helper.Test Results
cargo fmt --all -- --check: cleancargo check --workspace: cleancargo clippy --workspace -- -D warnings: cleancargo test -p hero_whiteboard_server: 3 passednode --checkon app.js, ui-helpers.js, properties.js: parses cleanlygrep navigator.clipboard: onlyapp.js::copyToClipboardandhome.html::copyLinkremain — both with.catch()andexecCommandfallbackThis is a UI-only behavior change; manual verification recommended:
http://[2001:db8::1]:9988/...).localhost/ HTTPS — confirm no regression.Implementation Summary
Four files changed, +63 / -11.
app.js(new helper)WhiteboardApp.copyToClipboard(text, successMsg)(defaultsuccessMsg = 'Copied!').fallbackCopy(text)builds an off-screen<textarea>withreadonly+aria-hidden, appliessetSelectionRange(0, text.length)(Safari iOS), wrapsdocument.execCommand('copy')intry/catch, and removes the node.navigator.clipboard.writeTextis available, the helper uses it and falls back tofallbackCopyon rejection. Otherwise it runsfallbackCopysynchronously.showToast('Copy failed — select the link and copy manually.', true)(red toast).showToast.home.html(copyLink)copyLink(url)with the same Clipboard API →execCommand→ red error toast pattern, inlined locally because the home page does not loadWhiteboardApp.onclick="copyLink('...')"call sites at lines 737 / 745 are untouched.ui-helpers.jsonclick="navigator.clipboard.writeText(document.getElementById('share-view-url').value);WhiteboardApp.showToast('Copied!')"(and theshare-edit-urlvariant) withonclick="WhiteboardApp.copyToClipboard(document.getElementById('share-view-url').value)". The helper handles its own toast.board.htmlui-helpers.js.properties.jsnavigator.clipboard.writeText(url); WhiteboardApp.showToast('Link copied!');with a singleWhiteboardApp.copyToClipboard(url, 'Link copied!');.Verification
cargo fmt --all -- --check: clean.cargo check --workspace: clean.cargo clippy --workspace -- -D warnings: clean.cargo test -p hero_whiteboard_server: 3 passed.node --checkon every modified JS module: clean.grep navigator.clipboard: onlyapp.js::copyToClipboardandhome.html::copyLinkremain, both behind a.catch()+execCommandfallback.Notes / caveats
document.execCommand('copy')is deprecated but still supported by every browser that runs the whiteboard UI today, and remains the canonical fallback for non-secure contexts.document.body(not the share modal) so a modal-close handler firing mid-copy cannot tear it out.