8a648c2ffc0e2c8a671a9074e00a0a3d56575589 Merge parents e6d1189bea4 67f660dc951 max Wed Sep 2 04:48:57 2026 -0700 Merge remote-tracking branch 'origin/master' # Conflicts: # src/hg/hgSession/hgSession.c diff --cc src/hg/hgSession/hgSession.c index bf8e16e50a8,8d9a2f6f8a8..11a3538c8d8 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@@ -1034,151 -1042,78 +1043,152 @@@ static void saveSessionJsonResult(struct sqlConnection *conn, char *encUserName, char *encSessionName, char *sessionName) /* 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". */ { struct dyString *dyUrl = dyStringNew(0); addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE); puts("Content-Type:application/json\n"); printf("{\"name\": \"%s\", \"url\": \"%s\"}\n", jsonStringEscape(sessionName), jsonStringEscape(dyUrl->string)); dyStringFree(&dyUrl); hDisconnectCentral(&conn); } +void doAnonNameJson() +/* AJAX endpoint that reserves a fresh, guaranteed-unique anonymous snapshot name and returns it as + * JSON {"name": ...} WITHOUT saving anything. The top-right "Share a link" dialog calls this on open + * so it can show the exact link as a preview before the user commits, while keeping name generation + * server-side (unique, crypto-strong) for every anonymous link. */ +{ +struct sqlConnection *conn = hConnectCentral(); +cartRemove(cart, hgsDoAnonName); +if (!sqlTableExists(conn, namedSessionTable)) + { + saveSessionJsonError(conn, "Required session table does not exist in the central database."); + return; + } +char *name = snapshotNewName(conn, "l"); +puts("Content-Type:application/json\n"); +printf("{\"name\": \"%s\"}\n", jsonStringEscape(name)); +hDisconnectCentral(&conn); +} + void doSaveSessionJson(char *userName) /* AJAX endpoint behind the "Share a link" menu button. Save the current cart as a named session * and print JSON {"name": <session name>, "url": <shareable link>}. When the user is not logged * in (or hgsShareAnon is set), save under the reserved anonymous user "l" with a random token - * name. When logged in with no name given, generate a short random name. Saved shared by link so - * the link works for anyone. Reuses saveCartAsSession() and addSessionLink(). */ + * name. When logged in, the caller supplies the name (a typed name, or a random internal + * "_XXXXXXXX" name generated client-side). Saved shared by link so the link works for anyone. + * Reuses saveCartAsSession() and addSessionLink(). */ { struct sqlConnection *conn = hConnectCentral(); if (!sqlTableExists(conn, namedSessionTable)) { saveSessionJsonError(conn, "Required session table does not exist in the central database."); return; } boolean anon = isEmpty(userName) || cgiBoolean(hgsShareAnon); +boolean failIfExists = cgiBoolean(hgsFailIfExists); +// A registered snapshot type (e.g. "blat") means: save a lightweight snapshot holding only that +// feature's declared cart vars, not the whole cart (see lib/snapshotSession.c). +char *snapshotType = cgiOptionalString(hgsSnapshotType); - // Read the requested name from the request, not the cart (hgSession's Save form leaves a sticky - // value in the cart under this same variable that would otherwise shadow ours). + // Read the requested name from the request, not the cart. cleanHgSessionFromCart() now takes + // this variable back out, but carts written before that still hold a sticky value from + // hgSession's Save form, and it would otherwise shadow ours. char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsNewSessionName, ""))); /* Keep our control variables out of the saved session contents and the user's own cart. */ cartRemove(cart, hgsDoSaveSessionJson); cartRemove(cart, hgsShareAnon); +cartRemove(cart, hgsFailIfExists); +cartRemove(cart, hgsSnapshotType); cartRemove(cart, hgsNewSessionName); cartRemove(cart, hgsNewSessionShare); +/* Snapshot path: a lightweight session holding only the feature's declared cart vars, under a + * server-generated, guaranteed-unique "__"-prefixed name (share tokens must never collide and + * silently overwrite one another). Handled before the normal full-session logic because its + * naming rules differ. Works for both anonymous ("l") and logged-in owners. */ +if (isNotEmpty(snapshotType)) + { + if (snapshotTypeFind(snapshotType) == NULL) + { + saveSessionJsonError(conn, "Unknown snapshot type."); + return; + } + char *snapUser = anon ? "l" : cgiEncodeFull(userName); + char *snapName; + if (isEmpty(sessionName)) + snapName = snapshotNewName(conn, snapUser); /* server-generated, unique */ + else if (startsWith(snapshotNamePrefix, sessionName)) + snapName = cgiEncodeFull(sessionName); + else + snapName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName)); + saveSnapshotSession(conn, snapshotType, snapUser, snapName, cart); + char *snapDecoded = cgiDecodeClone(snapName); + saveSessionJsonResult(conn, snapUser, snapName, snapDecoded); + return; + } + char *encUserName = NULL; char *encSessionName = NULL; if (anon) { encUserName = "l"; /* reserved anonymous user -> short link /s/l/<token> */ - sessionName = makeRandomKey(96); /* 16 URL-safe alphanumeric chars; no encoding needed */ - encSessionName = sessionName; + /* Every anonymous share uses the shared snapshot naming: a server-generated, guaranteed-unique + * "__"-token, so tokens never collide/overwrite and the reaper can garbage-collect 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 reap-eligible. */ + if (isEmpty(sessionName)) + encSessionName = snapshotNewName(conn, encUserName); + else if (startsWith(snapshotNamePrefix, sessionName)) + encSessionName = cgiEncodeFull(sessionName); + else + encSessionName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName)); + 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)) { - /* One-click share: auto-name the session. "_" is kept verbatim by cgiEncodeFull (unlike - * "-"), so the short link /s/<user>/<name> stays clean. */ - char randName[32]; - char *rk = makeRandomKey(48); /* 8 URL-safe alphanumeric chars */ - safef(randName, sizeof randName, "share_%s", rk); - freeMem(rk); - sessionName = cloneString(randName); + 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) + { + 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); } 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, "")));