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/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index 6962005757d..1ac413e611e 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -366,30 +366,34 @@
 if (gotSettings)
     sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, useCount, contents, settings from %s "
         "WHERE userName = '%s' ORDER BY sessionName;",
         namedSessionTable, encUserName);
 else
     sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, useCount, contents from %s "
         "WHERE userName = '%s' ORDER BY sessionName;",
         namedSessionTable, encUserName);
 sr = sqlGetResult(conn, query);
 
 int rowIdx = 0;
 
 while ((row = sqlNextRow(sr)) != NULL)
     {
     char *encSessionName = row[0];
+    /* A snapshot is a share token, not a session the user made and would recognize (see
+     * lib/snapshotSession.c).  Leave it out of the list, as its "__" prefix promises. */
+    if (snapshotIsSnapshotName(encSessionName))
+        continue;
     char *sessionName = cgiDecodeClone(encSessionName);
     char *link = NULL;
     int shared = atoi(row[1]);
     char *firstUse = row[2];
     char buf[512];
     boolean inGallery = FALSE;
     boolean hasDescription = FALSE;
 
     if (shared >=2)
         inGallery = TRUE;
 
     printf("<TR><TD>&nbsp;&nbsp;</TD><TD>");
 
     char iconId[256];
     char linkId[256];
@@ -1040,30 +1044,42 @@
 	  "database (%s).  Please ask a developer to create it using "
 	  "kent/src/hg/lib/namedSessionDb.sql .",
 	  namedSessionTable, sqlGetDatabase(conn));
 hDisconnectCentral(&conn);
 return dyStringCannibalize(&dyMessage);
 }
 
 static void saveSessionJsonError(struct sqlConnection *conn, char *message)
 /* Emit a JSON error response for the "Share a link" AJAX endpoints and disconnect. */
 {
 puts("Content-Type:application/json\n");
 printf("{\"error\": \"%s\"}\n", jsonStringEscape(message));
 hDisconnectCentral(&conn);
 }
 
+static boolean namedSessionExists(struct sqlConnection *conn, char *encUserName,
+                                  char *encSessionName)
+/* Is there already a session by this name for this user?  Both names must be encoded the way they
+ * are stored, i.e. through cgiEncodeFull(). */
+{
+char query[1024];
+sqlSafef(query, sizeof query,
+         "select count(*) from %s where userName = '%s' and sessionName = '%s'",
+         namedSessionTable, encUserName, encSessionName);
+return sqlQuickNum(conn, query) > 0;
+}
+
 static void saveSessionJsonResult(struct sqlConnection *conn, char *encUserName,
                                   char *encSessionName, char *sessionName, char *warning)
 /* Emit {"name": ..., "url": ...} for the "Share a link" AJAX endpoints and disconnect.
  * sessionName is the human-readable (decoded) name; the client uses it as the rename "old name".
  * warning (may be NULL) is added as "warning" for something that went wrong alongside a save that
  * did succeed, such as a thumbnail the server could not build. */
 {
 struct dyString *dyUrl = dyStringNew(0);
 addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE);
 puts("Content-Type:application/json\n");
 printf("{\"name\": \"%s\", \"url\": \"%s\"", jsonStringEscape(sessionName),
        jsonStringEscape(dyUrl->string));
 if (isNotEmpty(warning))
     printf(", \"warning\": \"%s\"", jsonStringEscape(warning));
 puts("}");
@@ -1158,86 +1174,99 @@
         {
         saveSessionJsonError(conn, "Unknown snapshot type.");
         return;
         }
     /* Refuse to mint a link that would reopen to nothing (e.g. BLAT results not built yet); tell the
      * caller to retry rather than handing out a dead link. */
     if (!snapshotHasRequired(st, cart))
         {
         saveSessionJsonError(conn, "These results are not ready yet. Please try again in a moment.");
         return;
         }
     char *snapUser = anon ? "l" : cgiEncodeFull(userName);
     char *snapName;
     if (isEmpty(sessionName))
         snapName = snapshotNewName(conn, snapUser);            /* server-generated, unique */
-    else if (startsWith(snapshotNamePrefix, sessionName))
+    else
+        {
+        if (startsWith(snapshotNamePrefix, sessionName))
             snapName = cgiEncodeFull(sessionName);
         else
             snapName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName));
+        /* Anonymous names are not the caller's to reuse; see the anon branch below. */
+        if (anon && namedSessionExists(conn, snapUser, snapName))
+            {
+            saveSessionJsonError(conn, "That link already exists.");
+            return;
+            }
+        }
     saveSnapshotSession(conn, snapshotType, snapUser, snapName, cart);
     char *snapDecoded = cgiDecodeClone(snapName);
     saveSessionJsonResult(conn, snapUser, snapName, snapDecoded, NULL);
     return;
     }
 
 char *encUserName = NULL;
 char *encSessionName = NULL;
 if (anon)
     {
     encUserName = "l";                    /* reserved anonymous user -> short link /s/l/<token> */
     /* Every anonymous share uses the shared snapshot naming: a server-generated, guaranteed-unique
      * "__"-token, so tokens never collide/overwrite and the snapshot cleaner can remove abandoned
      * ones.  The top-right Share dialog passes a name it just reserved (for its live preview); we
-     * force the "__" prefix either way so the link stays eligible for cleaning. */
+     * force the "__" prefix either way so the link stays eligible for cleaning.
+     *   A name that came with the request is only ever one the dialog just reserved, which does not
+     * exist yet.  Anonymous links all sit under the single reserved user "l", so a name already in
+     * the table stays as it is and the caller is told so, rather than being written over. */
     if (isEmpty(sessionName))
         encSessionName = snapshotNewName(conn, encUserName);
-    else if (startsWith(snapshotNamePrefix, sessionName))
+    else
+        {
+        if (startsWith(snapshotNamePrefix, sessionName))
             encSessionName = cgiEncodeFull(sessionName);
         else
             encSessionName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName));
+        if (namedSessionExists(conn, encUserName, encSessionName))
+            {
+            saveSessionJsonError(conn, "That link already exists.");
+            return;
+            }
+        }
     sessionName = cgiDecodeClone(encSessionName);   // keep decoded name in sync for the JSON result
     }
 else
     {
     /* Logged-in callers always supply a name: the caller either typed one or generated a random
      * internal "_XXXXXXXX" name client-side (sessRandomShareName in hgSession.js, shared by the
      * top-right "Share a link" menu in topLinks.js), so we no longer auto-name here. */
     if (isEmpty(sessionName))
         {
         saveSessionJsonError(conn, "Please provide a name for this session.");
         return;
         }
     encUserName = cgiEncodeFull(userName);
     encSessionName = cgiEncodeFull(sessionName);
     /* The Share dialog sets failIfExists when the user typed a custom name, so it can warn before
      * clobbering an existing session of theirs.  Report the clash instead of overwriting. */
-    if (failIfExists)
-        {
-        char query[1024];
-        sqlSafef(query, sizeof query,
-                 "select count(*) from %s where userName = '%s' and sessionName = '%s'",
-                 namedSessionTable, encUserName, encSessionName);
-        if (sqlQuickNum(conn, query) > 0)
+    if (failIfExists && namedSessionExists(conn, encUserName, encSessionName))
         {
         puts("Content-Type:application/json\n");
         printf("{\"exists\": true}\n");
         hDisconnectCentral(&conn);
         return;
         }
     }
-    }
 
 saveCartAsSession(conn, encUserName, encSessionName, 1);  /* shared by link */
 saveSessionJsonResult(conn, encUserName, encSessionName, sessionName, NULL);
 }
 
 void doRenameSessionJson(char *userName)
 /* AJAX endpoint for the "Specify name" step of the Share dialog: rename an existing session
  * (hgsOldSessionName -> hgsNewSessionName) under the current user.  Logged-in only.  Rejects a
  * name already in use rather than overwriting it.  Returns {"name","url"} or {"error"}. */
 {
 struct sqlConnection *conn = hConnectCentral();
 // Read the names from the request, not the cart: hgSession's Save form also uses these variables
 // and leaves a sticky value (e.g. the username) in the cart that would otherwise shadow ours.
 char *oldName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, "")));
 char *newName = trimSpaces(cloneString(cgiUsualString(hgsNewSessionName, "")));
@@ -2339,30 +2368,34 @@
                 "SELECT sessionName, shared, firstUse, useCount, contents, settings, lastUse FROM %s "
                 "WHERE userName = '%s' ORDER BY sessionName;", namedSessionTable, encUserName);
         else
             sqlSafef(query, sizeof(query),
                 "SELECT sessionName, shared, firstUse, useCount, contents, lastUse FROM %s "
                 "WHERE userName = '%s' ORDER BY sessionName;", namedSessionTable, encUserName);
         struct sqlResult *sr = sqlGetResult(conn, query);
         perfTimerStep(hgSessionTiming, "load sessions from MySQL");
         char **row;
         /* Cache one connection per assembly db so the per-session band/locus lookups don't
          * re-open a connection for every row when many sessions share an assembly. */
         struct hash *dbConnCache = hashNew(0);
         while ((row = sqlNextRow(sr)) != NULL)
             {
             char *encSessionName = row[0];
+            /* Snapshots are share tokens, not sessions the user made; keep them out of the list,
+             * as their "__" prefix promises (see lib/snapshotSession.c). */
+            if (snapshotIsSnapshotName(encSessionName))
+                continue;
             char *sessionName = cgiDecodeClone(encSessionName);
             int shared = atoi(row[1]);
             char *firstUse = cloneString(row[2]);
             struct tm firstUseTm;
             ZeroVar(&firstUseTm);
             strptime(firstUse, "%Y-%m-%d %T", &firstUseTm);
             long epoch = (long)mktime(&firstUseTm);
             /* created = date only for display; createdFull = date+minute for the hover. */
             char *dateOnly = cloneString(firstUse);
             char *spacePt = strchr(dateOnly, ' ');
             if (spacePt != NULL)
                 *spacePt = '\0';
             char *createdFull = cloneString(firstUse);
             if (strlen(createdFull) == 19)
                 createdFull[16] = '\0';