5a249cd50f592a3b7598110eec792cfc942fab3f max Wed Sep 9 08:52:06 2026 -0700 Address the v504 code review hgSession: an anonymous share name that arrives with the request is saved only when it is not already in the table. Every anonymous link sits under the one reserved user "l", so a name already there stays as it is and the caller is told so. The top-right Share dialog is unaffected, since it passes a name it has just reserved and such a name does not exist yet. Snapshot names are now left out of both My Sessions listings, which is what their "__" prefix has claimed all along. Share dialog: "Create link & copy" reports the copy instead of promising it. copyToClipboard says whether the text reached the clipboard, the dialog passes that on when a browser refuses, and it tries the asynchronous clipboard API before giving up. The preview is built with the same encoding the server uses, so a name holding a hyphen or a slash previews as the link that really gets made. Cancelling out of the name editor no longer copies a second time, and a reply with no link in it says so rather than showing "undefined". hgBlat: a second click on the share button while the first request is still out no longer mints a second snapshot session, and a box dismissed during the wait stays closed. Also: a snapshot moves a cart value into durable storage only when it is a trash path, the way sessionData's own callers check; sqlAddressMatch keeps to its own documented precondition when handed an empty address; alphaGenomeToWig compares its output with its input rather than with itself, rejects a position that is not all digits and skips an empty score; and hgc's default iframe width reaches the browser as one percent sign. refs #38294 diff --git src/hg/js/topLinks.js src/hg/js/topLinks.js index b16377e50a7..312058fa024 100644 --- src/hg/js/topLinks.js +++ src/hg/js/topLinks.js @@ -210,69 +210,97 @@ note.appendChild(document.createTextNode("You are not logged in. ")); var loginEl = document.getElementById("loginLink"); note.appendChild(el("a", {href: loginEl ? loginEl.getAttribute("href") : "../cgi-bin/hgLogin", textContent: "Log in"})); note.appendChild(document.createTextNode( " to give your links a name and to edit or update them later.")); } body.appendChild(note); } // Show the resulting share URL with a "Copy to clipboard" button. opts (session mode): // {name: <current session name>, session: true, loggedIn: <bool>}. A logged-in session link // also gets a "Specify name" button. url mode passes no opts → just the link + Copy. function showResult(body, url, opts) { opts = opts || {}; + // No link to show. postJson also routes a name-clash reply here, and only the create + // step knows what to do with one, so say so rather than printing "undefined" as the link. + if (!url) { + body.innerHTML = ""; + body.appendChild(el("p", {textContent: "Could not create the link. Please try again."}, + {marginTop: "0", color: "#a00"})); + return; + } var canRename = opts.session && opts.loggedIn; body.innerHTML = ""; // Saved sessions and plain page URLs never expire; an anonymous snapshot link (the BLAT // alignment page's share, opts.snapshot) is durable while used but cleaned after years of // no use, so only that case makes the softer promise and points at sessions for permanence. var durability = opts.snapshot ? "The link remains valid for years; to keep your results permanently, save them " + "into a Session:" : "Links never time out:"; body.appendChild(el("p", {textContent: "You can share this link with collaborators, put " + "it into figure legends or manuscripts. " + durability}, {marginTop: "0"})); // Read-only text region (not an <input>) so it's clear the URL isn't meant to be edited. var urlBox = el("div", {id: "tlShareUrl", textContent: url}, {background: "#f0f0f0", padding: "6px 8px", borderRadius: "4px", wordBreak: "break-all", userSelect: "all", fontFamily: "monospace"}); urlBox.setAttribute("data-copy", url); // copyToClipboard() reads data-copy or innerText body.appendChild(urlBox); var btnRow = el("div", {}, {marginTop: "8px"}); var copyBtn = el("button", {title: "Copy URL to clipboard"}); copyBtn.setAttribute("data-target", "tlShareUrl"); copyBtn.innerHTML = clipboardSvg + "Copy to clipboard"; + var copied = false; copyBtn.addEventListener("click", function(ev) { - if (typeof copyToClipboard === "function") copyToClipboard(ev); + if (typeof copyToClipboard === "function") + copied = copyToClipboard(ev); }); btnRow.appendChild(copyBtn); - // One-click "Create link & copy": copy right after the session is created. execCommand copy - // still runs while the modal is focused; if a browser blocks it the URL box and Copy button - // above are the manual fallback. - if (opts.autoCopy) - copyBtn.click(); - if (canRename) { var nameBtn = el("button", {textContent: "Specify name"}, {marginLeft: "8px"}); nameBtn.addEventListener("click", function() { showRename(body, url, opts); }); btnRow.appendChild(nameBtn); } body.appendChild(btnRow); + // One-click "Create link & copy": copy now that the button is on the page. The copy runs + // out of the reply to the save request rather than out of a click of the user's own, and a + // browser may refuse it on those grounds, so say which of the two happened instead of + // promising the clipboard either way. autoCopy is spent here: coming back to this view, + // e.g. by cancelling out of the name editor, must not copy a second time. + if (opts.autoCopy) { + opts.autoCopy = false; + copyBtn.click(); + if (!copied) { + var note = el("p", {textContent: "Your browser did not allow the copy. Use the " + + "button above to copy the link."}, {marginTop: "8px", color: "#a00"}); + body.appendChild(note); + // The asynchronous clipboard API does not need a click of the user's, so it can + // still get there in a browser that grants the permission. Only then is the + // warning wrong, so take it back. + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(url).then(function() { + copyBtn.innerHTML = "Copied"; + note.remove(); + }, function() { }); + } + } + } + if (opts.session) appendManageNote(body, opts.loggedIn); else if (opts.pageNote) body.appendChild(el("p", {textContent: "This link shows the page only. It does not " + "restore the tracks you currently have displayed."}, {marginTop: "14px"})); } // The "Specify name" editor: rename the session, then show the updated link. function showRename(body, currentUrl, opts) { body.innerHTML = ""; body.appendChild(el("p", {textContent: "Name this link:"}, {marginTop: "0"})); var nameInput = el("input", {type: "text", value: "", placeholder: "fig3b"}, {width: "100%", padding: "4px", margin: "6px 0", boxSizing: "border-box"}); body.appendChild(nameInput); var status = el("div", {}, {margin: "6px 0"}); @@ -397,55 +425,69 @@ function randChars(n) { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var s = ""; for (var i = 0; i < n; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)); return s; } // The default session name for a logged-in share: a leading "_" (marking it machine-generated, // kept verbatim by the short-link encoder) plus 8 chars, mirroring sessRandomShareName() in // hgSession.js. Anonymous names are server-generated, not built here. function shareName() { return "_" + randChars(8); } + // Encode a session name the way the server does, i.e. cgiEncodeFull() in lib/cheapcgi.c: a + // letter, a digit, "." and "_" stay, everything else becomes %XX. encodeURIComponent keeps + // seven more characters than that, "-" among them, so a name like "fig3-b" would be previewed + // as a link the server never makes. + function cgiEncodeFull(s) { + return encodeURIComponent(s).replace(/[-!~*'()]/g, function(c) { + return "%" + c.charCodeAt(0).toString(16).toUpperCase(); + }); + } + // Build the exact share URL for a given session name, matching addSessionLink() in hgSession.c: // a short "/s/<user>/<name>" link when hgSession.shortLink is on, otherwise the long hgTracks // hgS_doOtherUser form. Logged out, the owner is the reserved anonymous user "l". function shareUrlFor(opts, name) { var user = opts.loggedIn ? opts.userName : "l"; + var encUser = cgiEncodeFull(user); + var encName = cgiEncodeFull(name); var origin = window.location.protocol + "//" + window.location.host; - if (opts.shortLink) - return origin + "/s/" + encodeURIComponent(user) + "/" + encodeURIComponent(name); + // A "/" in either name rules out the short form, since apache's redirect splits the path on + // it; addSessionLink() falls back to the long link in that case, so the preview must too. + if (opts.shortLink && encUser.indexOf("%2F") < 0 && encName.indexOf("%2F") < 0) + return origin + "/s/" + encUser + "/" + encName; return origin + "/cgi-bin/hgTracks?hgS_doOtherUser=submit&hgS_otherUserName=" + - encodeURIComponent(user) + "&hgS_otherUserSessionName=" + encodeURIComponent(name); + encUser + "&hgS_otherUserSessionName=" + encName; } // The initial session-mode view: preview the final link (not yet active), then a single button // that creates the shared session and copies the link in one step. We generate the session name // here and pass it to the server so the previewed link is exactly the one that gets created. // Logged in, the user may type a custom name before creating; the preview updates as they type. // initialName (optional) pre-fills the name field, e.g. when returning from the overwrite prompt. // presetAutoName (optional) is the name to use when none is typed - for anonymous shares this is // the unique name reserved from the server; logged in we generate a client-side "_XXXX" name. function showCreatePrompt(body, opts, initialName, presetAutoName) { body.innerHTML = ""; var autoName = presetAutoName || shareName(); body.appendChild(el("p", {textContent: "This link points to your current view and never " + - "times out. It becomes active — and is copied to your clipboard — when you click the " + - "button below."}, {marginTop: "0"})); + "times out. It becomes active when you click the button below, which also copies it " + + "to your clipboard."}, {marginTop: "0"})); // Optional custom name (logged in only): leave it blank to use the generated name above. // A short, space-free name keeps the link tidy (it becomes part of the URL). var nameInput = null; if (opts.loggedIn) { body.appendChild(el("label", {textContent: "Name this link (optional):"}, {display: "block", margin: "10px 0 4px", fontWeight: "bold"})); nameInput = el("input", {type: "text", placeholder: "fig3b", value: initialName || ""}, {width: "100%", padding: "4px", boxSizing: "border-box"}); body.appendChild(nameInput); } // Preview of the link, styled muted to signal it is not active yet; updated live below. var urlBox = el("div", {}, {background: "#f0f0f0", padding: "6px 8px", borderRadius: "4px", wordBreak: "break-all", fontFamily: "monospace", color: "#666", margin: "8px 0"});