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';