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"});