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("
| | ");
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/ */
/* 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';
|