c5a326f3cc42beca0b59c284a2819763548c166a max Wed Jul 8 05:31:13 2026 -0700 Add top-right "Share a link" and "Login" links to the menu bar, refs #10138 New js/topLinks.js drives two links at the top-right of the menu bar on all pages: - Login: links to the login page when logged out; when logged in it shows the username and opens an account dialog (My Sessions / My Custom Tracks / My Track Hubs, change password, sign out). - Share a link: on hgTracks, one click saves the current view as a session and shows a copyable short link, with an optional "Specify name" rename step; works logged-in or anonymously (reserved user "l"). On hgTrackUi and the hgc/hgGene item-details popup it instead shares the page URL with the hgsid stripped and the db argument kept. hgSession gains two JSON endpoints (hgS_doSaveSessionJson, hgS_doRenameSessionJson) that reuse saveCartAsSession() and addSessionLink(). The menu bar is patched in lib/web.c (CGI pages) and hgMenubar.c (static pages) via comment placeholders in globalNavBar.inc. On narrow/phone screens the links collapse into a menu icon with a dropdown so they no longer overlap the menu. diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c index 9d03630d62b..eb96e088e86 100644 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@ -28,30 +28,31 @@ #include "ra.h" #include "wikiLink.h" #include "customTrack.h" #include "customFactory.h" #include "udc.h" #include "hgSession.h" #include "hgConfig.h" #include "sessionThumbnail.h" #include "filePath.h" #include "obscure.h" #include "trashDir.h" #include "hubConnect.h" #include "trackHub.h" #include "errCatch.h" #include "sessionData.h" +#include "jsonParse.h" char *database = NULL; void usage() /* Explain usage and exit. */ { errAbort( "hgSession - Interface with wiki login and do session saving/loading.\n" "usage:\n" " hgSession \n" ); } /* Global variables. */ struct cart *cart; @@ -987,30 +988,166 @@ { printShareMessage(dyMessage, encUserName, encSessionName, FALSE); } cartCheckForCustomTracks(cart, dyMessage); } else dyStringPrintf(dyMessage, "Sorry, required table %s does not exist yet in the central " "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 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 doSaveSessionJson(char *userName) +/* AJAX endpoint behind the "Share a link" menu button. Save the current cart as a named session + * and print JSON {"name": , "url": }. 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(). */ +{ +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); +// 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). +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, hgsNewSessionName); +cartRemove(cart, hgsNewSessionShare); + +char *encUserName = NULL; +char *encSessionName = NULL; +if (anon) + { + encUserName = "l"; /* reserved anonymous user -> short link /s/l/ */ + sessionName = makeRandomKey(96); /* 16 URL-safe alphanumeric chars; no encoding needed */ + encSessionName = sessionName; + } +else + { + if (isEmpty(sessionName)) + { + /* One-click share: auto-name the session. "_" is kept verbatim by cgiEncodeFull (unlike + * "-"), so the short link /s// 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); + } + encUserName = cgiEncodeFull(userName); + encSessionName = cgiEncodeFull(sessionName); + } + +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, ""))); +char *newName = trimSpaces(cloneString(cgiUsualString(hgsNewSessionName, ""))); +cartRemove(cart, hgsDoRenameSessionJson); +cartRemove(cart, hgsOldSessionName); +cartRemove(cart, hgsNewSessionName); + +if (isEmpty(userName)) + { + saveSessionJsonError(conn, "Please log in to name this link."); + return; + } +if (isEmpty(oldName) || isEmpty(newName)) + { + saveSessionJsonError(conn, "Please enter a name."); + return; + } + +char *encUserName = cgiEncodeFull(userName); +char *encOldName = cgiEncodeFull(oldName); +char *encNewName = cgiEncodeFull(newName); +char query[1024]; + +if (sameString(oldName, newName)) + { + saveSessionJsonResult(conn, encUserName, encNewName, newName); + return; + } + +/* Reject a name the user is already using (don't clobber an existing saved session). */ +sqlSafef(query, sizeof query, "select count(*) from %s where userName = '%s' and sessionName = '%s'", + namedSessionTable, encUserName, encNewName); +if (sqlQuickNum(conn, query) > 0) + { + saveSessionJsonError(conn, "You already have a session with that name. Please pick another."); + return; + } +sqlSafef(query, sizeof query, "select count(*) from %s where userName = '%s' and sessionName = '%s'", + namedSessionTable, encUserName, encOldName); +if (sqlQuickNum(conn, query) == 0) + { + saveSessionJsonError(conn, "Could not find the link to rename."); + return; + } + +/* Same UPDATE that doSessionChange uses to rename a session. */ +sqlSafef(query, sizeof query, + "UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';", + namedSessionTable, encNewName, encUserName, encOldName); +sqlUpdate(conn, query); + +saveSessionJsonResult(conn, encUserName, encNewName, newName); +} + int thumbnailAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn, struct dyString *dyMessage) /* Create a thumbnail image for the gallery. If the necessary tools can't be found, * add a warning message to dyMessage unless the hg.conf setting * sessionThumbnail.suppressWarning is set to "on". * Leaks memory from a generated filename string, plus a couple of dyStrings. * Returns without determining if image creation succeeded (it happens in a separate * thread); the return value is 0 if a message was added to dyMessage, otherwise it's 1. */ { char query[4096]; char *suppressConvert = cfgOption("sessionThumbnail.suppress"); if (suppressConvert != NULL && sameString(suppressConvert, "on")) return 1; char *convertPath = cfgOption("sessionThumbnail.convertPath"); @@ -1920,30 +2057,38 @@ launchForeAndBackGround("makeDownloadSessionCtData"); exit(0); } } else if (doDownloadList) doDownloadSessionCtData(doDownloadList); else if (cartVarExists(cart, hgsDoMainPage) || cartVarExists(cart, hgsCancel)) doMainPage(userName, NULL); else if (cartVarExists(cart, hgsDoNewSession)) { char *message = doNewSession(userName); doMainPage(userName, message); } +else if (cartVarExists(cart, hgsDoSaveSessionJson)) + { + doSaveSessionJson(userName); + } +else if (cartVarExists(cart, hgsDoRenameSessionJson)) + { + doRenameSessionJson(userName); + } else if (cartVarExists(cart, hgsDoOtherUser)) { char *message = doOtherUser(hgsDoOtherUser); doMainPage(userName, message); } else if (cartVarExists(cart, hgsDoSaveLocal)) { doSaveLocal(); } else if (cartVarExists(cart, hgsDoLoadLocal)) { char *message = doLoad(FALSE, hgsDoLoadLocal); doMainPage(userName, message); } else if (cartVarExists(cart, hgsDoLoadUrl))