9f8d33c8b2b6bc61f6d02d781c4e02836f7099f9 max Fri Aug 21 02:05:42 2026 -0700 hgSession: new opt-in JavaScript "My Sessions" page; share gbModern.css with hgBlat. refs #38157 Applies the hgBlat facelift strategy (#37996) to hgSession: an opt-in, client-rendered "My Sessions" page gated by the sessionNewPage / sessionNewPageBanner hg.conf flags (mirroring blatNewForm / blatNewFormBanner), with a banner linking between the classic and new pages so neither is a one-way door. sessionNewPage also flips the site default. hgSession.c stays the data/action backend: it emits the session list and page config as an inline JSON global (hgSessionData) into an empty #sessionApp container, and the new hgSession.js builds the UI - a save-current-view card (name + optional description + "only I can load it"; empty name saves under a random share_ name), a "most recently saved session" one-click Update, a searchable/sortable/paged DataTable of sessions (assembly + position, created with last-used on hover, views, a lock icon on private sessions), inline Share (copy link / email / gallery), Edit (rename + description + private), Overwrite and Delete, and a bulk Select -> Delete-all-selected mode. The mutating actions POST to new JSON endpoints (hgS_doDeleteJson / doShareJson / doGalleryJson / doOverwriteJson / doDescribeJson) that run the same SQL as the classic full-page handlers and return JSON, so the table updates in place; loads, file up/downloads and custom-track backup stay as ordinary form submits/links. The Advanced panel keeps feature parity with the classic page (load another user's session, load from URL/file, save to file, back up custom tracks, reset), minus the login/ change-password links that now live in the top menu. Shared UCSC house-style components (design tokens, .gbPill, .gbCard, .gbStrip, .gbSection, .gbShareBox, .gbBanner, the .gbModal* dialog and a .gbTable) are factored into a new gbModern.css. hgBlat is migrated onto it: its generic .blat* classes are renamed to the shared .gb* names in hgBlat.css / hgBlat.js and the #blatResults / #blatFormBox containers get class="gbApp"; verified pixel-clean against the previous search form and results pages, including the rename modal. hgSession.css holds only session-specific layout. diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c index 2494423d8b5..e1230acd091 100644 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@ -29,50 +29,58 @@ #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" +#include "jsonWrite.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; char *excludeVars[] = {"Submit", "submit", hgsSessionDataDbSuffix, NULL}; /* Javascript to confirm that the user truly wants to delete a session. */ #define confirmDeleteFormat "return confirm('Are you sure you want to delete ' + decodeURIComponent('%s') + '?');" +/* Forward declarations for the experimental client-rendered Sessions page (hgSession.js), which is + * an opt-in alternative gated by the sessionNewPage / sessionNewPageBanner hg.conf flags, mirroring + * hgBlat's blatNewForm / blatNewFormBanner facelift. Defined below. */ +static boolean sessionNewPageActive(); +static void printSessionNewPageBanner(boolean onNewPage); +void doMainPageNew(char *userName, char *message); + char *cgiDecodeClone(char *encStr) /* Allocate and return a CGI-decoded copy of encStr. */ { size_t len = strlen(encStr); char *decStr = needMem(len+1); cgiDecode(encStr, decStr, len); return decStr; } void welcomeUser(char *wikiUserName) /* Tell the user they are not logged in to the wiki or other login * system and tell them how to do so. */ { char *wikiHost = wikiLinkHost(); @@ -628,30 +636,32 @@ /* If userName is non-null, show sessions that belong to user and allow * saving of named sessions. * If savedSessionsSupported, allow import of named sessions. * Allow export/import of settings from file/URL. */ { char *formMethod = cartUsualString(cart, "formMethod", "POST"); if (webStarted) webNewSection("Session Management"); else { cartWebStart(cart, NULL, "Session Management"); jsInit(); } +printSessionNewPageBanner(FALSE); + printf("

See the Sessions User's Guide " "for more information about this tool. " "See the Session Gallery " "for example sessions.

\n"); showCartLinks(); printf("
\n", hgSessionName(), formMethod); cartSaveSession(cart); struct slName *existingSessionNames = NULL; @@ -713,30 +723,35 @@ "%sU where U is the URL of your " "settings file, e.g. http://www.mysite.edu/~me/mySession.txt . " "In this type of link, you can replace " "\"hgSession\" with \"hgTracks\" in order to proceed directly to " "the Genome Browser. For an example page using such links " "please see the Session Gallery.\n", dyUrl->string); printf("\n"); dyStringFree(&dyUrl); } void doMainPage(char *userName, char *message) /* Login status/links and session controls. */ { +if (sessionNewPageActive()) + { + doMainPageNew(userName, message); + return; + } puts("Content-Type:text/html\n"); if (loginSystemEnabled() || wikiLinkEnabled()) { if (userName) welcomeUser(userName); else offerLogin(); if (isNotEmpty(message)) { if (cartVarExists(cart, hgsDoSessionDetail)) webNewSection("Session Details"); else webNewSection("Updated Session"); puts(message); } @@ -1992,30 +2007,494 @@ errAbort("The contents of the uploaded file are no longer available. " "Please choose the file again."); struct tempName tn; trashDirFile(&tn, "backGround", cartSessionId(cart), ".bin"); writeGulp(tn.forCgi, mem, size); // add new cart var with trash path // hgS_extractUpload_hub_9614_Anc11__filepath char *varName = replaceChars(binaryParam, "__binary", "__filepath"); cartRemove(cart, varName); // just in case cartSetString(cart, varName, tn.forCgi); // update the cart } +/* --------------------------------------------------------------------------------------------- + * Experimental client-rendered "Sessions" page (hgSession.js). + * + * An opt-in modern alternative to the classic server-rendered page, applying hgBlat's facelift + * strategy (#37996): hgSession.c stays the data/action backend, emits the session list and page + * config as an inline JSON global (hgSessionData) into an empty container, and hgSession.js builds + * the UI. Gated exactly like hgBlat's blatNewForm/blatNewFormBanner: + * - sessionNewPage (hg.conf bool, default off; also a cart var so a user's choice sticks + * and the banner links can flip it) selects which UI is the default. + * - sessionNewPageBanner (hg.conf bool, default = sessionNewPage) turns on the old<->new notes. + * The inline table actions (delete/share/gallery/overwrite/describe) each POST to a small JSON + * endpoint below that runs the same SQL the classic full-page handlers do, but returns JSON so the + * table can update in place. Navigation actions (load, load from URL/file, save-local, backup, + * reset) stay as ordinary form submits/links that hgSession.js builds against the existing actions. + * --------------------------------------------------------------------------------------------- */ + +static boolean sessionNewPageActive() +/* TRUE when the experimental client-rendered Sessions page should be shown. Cart variable + * sessionNewPage (set by the banner links) wins, defaulting to the hg.conf flag of the same name. */ +{ +return cartUsualBoolean(cart, "sessionNewPage", + cfgOptionBooleanDefault("sessionNewPage", FALSE)); +} + +static void printSessionNewPageBanner(boolean onNewPage) +/* Emit the note that links between the classic and the experimental pages, so neither is a one-way + * door. Shown wherever sessionNewPageBanner is on (defaulting to sessionNewPage), like hgBlat's + * printNewFormBanner. On the new page gbModern.css supplies .gbBanner; the classic page does not + * load it, so emit a small inline style there. */ +{ +if (!cfgOptionBooleanDefault("sessionNewPageBanner", + cfgOptionBooleanDefault("sessionNewPage", FALSE))) + return; +if (onNewPage) + printf("
You are using the new experimental Sessions page. " + "Return to the classic page. " + "If you have feedback, please let us know at " + "genome@soe.ucsc.edu.
\n", + cartSessionVarName(), cartSessionId(cart)); +else + { + printf("\n"); + printf("
We are testing a new Sessions page, with a searchable, " + "sortable table and one-click sharing. " + "Try the new page.
\n", + cartSessionVarName(), cartSessionId(cart)); + } +} + +static char *sessionValFromContents(char *contents, char *var) +/* Extract the value of var (e.g. "db" or "position") from a saved session's CGI-encoded cart + * contents string, CGI-decoded, or NULL if not present. */ +{ +if (isEmpty(contents)) + return NULL; +char pfx[64]; +char *valIdx = NULL; +safef(pfx, sizeof(pfx), "%s=", var); +if (startsWith(pfx, contents)) + valIdx = contents + strlen(pfx); +else + { + char ampPfx[66]; + safef(ampPfx, sizeof(ampPfx), "&%s", pfx); + char *p = strstr(contents, ampPfx); + if (p != NULL) + valIdx = p + strlen(ampPfx); + } +if (valIdx == NULL) + return NULL; +char *valEnd = strchr(valIdx, '&'); +char *enc = valEnd ? cloneStringZ(valIdx, valEnd - valIdx) : cloneString(valIdx); +char *dec = cgiDecodeClone(enc); +freez(&enc); +return dec; +} + +static int countShownTracks(struct cart *cart) +/* Rough count of tracks turned on in the current cart: cart entries whose value is a display + * visibility other than hide. A proxy for "tracks currently shown" for the save-summary line + * (may over/undercount some composite subtracks); good enough for a hint. */ +{ +int n = 0; +struct hashEl *list = hashElListHash(cart->hash), *el; +for (el = list; el != NULL; el = el->next) + { + char *v = (char *)el->val; + if (v && (sameString(v, "dense") || sameString(v, "squish") || sameString(v, "pack") || + sameString(v, "full") || sameString(v, "show"))) + n++; + } +hashElFreeList(&list); +return n; +} + +static void sessionDataToJson(char *userName, struct jsonWrite *jw) +/* Fill jw (an open object) with { config:{...}, sessions:[...] } for the experimental page. */ +{ +boolean loggedIn = isNotEmpty(userName); +boolean loginAvail = (loginSystemEnabled() || wikiLinkEnabled()); + +jsonWriteObjectStart(jw, "config"); +jsonWriteBoolean(jw, "loggedIn", loggedIn); +jsonWriteBoolean(jw, "loginAvail", loginAvail); +if (loggedIn) + jsonWriteString(jw, "userName", userName); +jsonWriteString(jw, "hgsid", cartSessionId(cart)); +jsonWriteString(jw, "cartVar", cartSessionVarName()); +/* Current view being saved, for the save-summary line. Show the assembly accession, not the + * internal "hub__" name, for assembly hubs (trackHubSkipHubName). */ +char *db = cartUsualString(cart, "db", NULL); +if (isNotEmpty(db)) + jsonWriteString(jw, "db", trackHubSkipHubName(db)); +char *pos = cartUsualString(cart, "position", NULL); +if (isNotEmpty(pos)) + jsonWriteString(jw, "position", pos); +int trackCount = countShownTracks(cart); +if (trackCount > 0) + jsonWriteNumber(jw, "trackCount", trackCount); +/* Login / account URLs so the JS can render a compact account line. */ +if (loginAvail) + { + if (!loggedIn) + jsonWriteString(jw, "loginUrl", wikiLinkUserLoginUrl(cartSessionId(cart))); + else + { + jsonWriteString(jw, "logoutUrl", wikiLinkUserLogoutUrl(cartSessionId(cart))); + if (!loginUseBasicAuth()) + jsonWriteString(jw, "changePasswordUrl", wikiLinkChangePasswordUrl(cartSessionId(cart))); + } + jsonWriteString(jw, "signupUrl", wikiLinkUserSignupUrl(cartSessionId(cart))); + } +jsonWriteStringf(jw, "classicUrl", "hgSession?sessionNewPage=0&%s=%s", + cartSessionVarName(), cartSessionId(cart)); +jsonWriteString(jw, "helpUrl", "../goldenPath/help/hgSessionHelp.html"); +jsonWriteString(jw, "galleryUrl", "../goldenPath/help/sessions.html"); +jsonWriteStringf(jw, "publicSessionsUrl", "../cgi-bin/hgPublicSessions?%s", cartSidUrlString(cart)); +/* Reset-to-defaults link, same as showCartLinks(). */ +char returnAddress[512]; +safef(returnAddress, sizeof(returnAddress), "%s?%s", hgSessionName(), cartSidUrlString(cart)); +jsonWriteStringf(jw, "resetUrl", "../cgi-bin/cartReset?%s&destination=%s", + cartSidUrlString(cart), cgiEncodeFull(returnAddress)); +jsonWriteObjectEnd(jw); // config + +jsonWriteListStart(jw, "sessions"); +if (loggedIn) + { + struct sqlConnection *conn = hConnectCentral(); + if (sqlTableExists(conn, namedSessionTable)) + { + char *encUserName = cgiEncodeFull(userName); + boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0); + char query[512]; + if (gotSettings) + sqlSafef(query, sizeof(query), + "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); + char **row; + while ((row = sqlNextRow(sr)) != NULL) + { + char *encSessionName = row[0]; + 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); + char *dateOnly = cloneString(firstUse); + char *spacePt = strchr(dateOnly, ' '); + if (spacePt != NULL) + *spacePt = '\0'; + char *db2 = sessionValFromContents(row[4], "db"); + char *pos2 = sessionValFromContents(row[4], "position"); + char *description = gotSettings ? getSetting(row[5], "description") : NULL; + /* lastUse (last time the session was saved/overwritten/loaded) drives the "most + * recently saved session" quick-update shortcut on the client. */ + char *lastUse = cloneString(row[gotSettings ? 6 : 5]); + struct tm lastUseTm; + ZeroVar(&lastUseTm); + strptime(lastUse, "%Y-%m-%d %T", &lastUseTm); + long lastUseEpoch = (long)mktime(&lastUseTm); + /* Trim the seconds off the display: "2026-08-21 10:29:25" -> "2026-08-21 10:29". */ + if (strlen(lastUse) == 19) + lastUse[16] = '\0'; + struct dyString *dyUrl = dyStringNew(0); + addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE); + + jsonWriteObjectStart(jw, NULL); + jsonWriteString(jw, "name", sessionName); + jsonWriteString(jw, "encName", encSessionName); + jsonWriteNumber(jw, "shared", shared); + jsonWriteString(jw, "created", dateOnly); + jsonWriteNumber(jw, "createdEpoch", epoch); + jsonWriteString(jw, "lastUse", lastUse); + jsonWriteNumber(jw, "lastUseEpoch", lastUseEpoch); + jsonWriteNumber(jw, "useCount", atoll(row[3])); + if (isNotEmpty(db2)) + jsonWriteString(jw, "db", trackHubSkipHubName(db2)); + if (isNotEmpty(pos2)) + jsonWriteString(jw, "position", pos2); + if (isNotEmpty(description)) + jsonWriteString(jw, "description", description); + jsonWriteString(jw, "shareUrl", dyUrl->string); + jsonWriteObjectEnd(jw); + + dyStringFree(&dyUrl); + freez(&firstUse); + freez(&dateOnly); + freez(&sessionName); + } + sqlFreeResult(&sr); + } + hDisconnectCentral(&conn); + } +jsonWriteListEnd(jw); // sessions +} + +void doMainPageNew(char *userName, char *message) +/* Render the experimental client-rendered Sessions page: framework header (gold "My Sessions" + * band), the experimental banner, an empty #sessionApp container, and the hgSessionData JSON that + * hgSession.js reads to build the UI. */ +{ +puts("Content-Type:text/html\n"); +cartWebStart(cart, NULL, "My Sessions"); +jsInit(); +jsIncludeDataTablesLibs(); +webIncludeResourceFile("gbModern.css"); +webIncludeResourceFile("hgSession.css"); +jsIncludeFile("hgSession.js", NULL); + +printSessionNewPageBanner(TRUE); +if (isNotEmpty(message)) + printf("
%s
\n", message); + +printf("
\n"); + +struct jsonWrite *jw = jsonWriteNew(); +jsonWriteObjectStart(jw, NULL); +sessionDataToJson(userName, jw); +jsonWriteObjectEnd(jw); +jsInlineF("var hgSessionData = %s;\n", jw->dy->string); +jsonWriteFree(&jw); + +cartWebEnd(); +} + +/* ---- JSON action endpoints for the experimental page's inline table actions ---- */ + +static void saveSessionJsonOk(struct sqlConnection *conn, char *extraFields) +/* Emit {"success": true[, ]} and disconnect. extraFields (may be NULL) is inserted + * verbatim after "success": true, e.g. ", \"shared\": 2". */ +{ +puts("Content-Type:application/json\n"); +printf("{\"success\": true%s}\n", extraFields ? extraFields : ""); +hDisconnectCentral(&conn); +} + +void doDeleteSessionJson(char *userName) +/* AJAX: delete the session named by hgsOldSessionName under the current user. */ +{ +struct sqlConnection *conn = hConnectCentral(); +char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, ""))); +if (isEmpty(userName)) + { saveSessionJsonError(conn, "Please log in and try again."); return; } +if (isEmpty(sessionName)) + { saveSessionJsonError(conn, "No session was specified."); return; } +char *encUserName = cgiEncodeFull(userName); +char *encSessionName = cgiEncodeFull(sessionName); +char query[512]; +sqlSafef(query, sizeof(query), "select shared from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +int shared = sqlQuickNum(conn, query); +if (shared >= 2) + thumbnailRemove(encUserName, encSessionName, conn); +sqlSafef(query, sizeof(query), "DELETE FROM %s WHERE userName = '%s' AND sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +sqlUpdate(conn, query); +saveSessionJsonOk(conn, NULL); +} + +void doShareSessionJson(char *userName) +/* AJAX: set the "shared by link" flag (0<->1) on hgsOldSessionName. Desired state in + * hgsNewSessionShare (0/1). Does not touch the gallery (shared==2) except to unshare. */ +{ +struct sqlConnection *conn = hConnectCentral(); +char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, ""))); +int desired = cgiUsualInt(hgsNewSessionShare, 0); +cartRemove(cart, hgsNewSessionShare); +if (isEmpty(userName)) + { saveSessionJsonError(conn, "Please log in and try again."); return; } +if (isEmpty(sessionName)) + { saveSessionJsonError(conn, "No session was specified."); return; } +char *encUserName = cgiEncodeFull(userName); +char *encSessionName = cgiEncodeFull(sessionName); +char query[512]; +sqlSafef(query, sizeof(query), "select shared from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +int shared = sqlQuickNum(conn, query); +int newShared = desired ? 1 : 0; +sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d WHERE userName = '%s' AND sessionName = '%s';", + namedSessionTable, newShared, encUserName, encSessionName); +sqlUpdate(conn, query); +sessionTouchLastUse(conn, encUserName, encSessionName); +if (newShared == 0 && shared >= 2) + thumbnailRemove(encUserName, encSessionName, conn); +char extra[32]; +safef(extra, sizeof(extra), ", \"shared\": %d", newShared); +saveSessionJsonOk(conn, extra); +} + +void doGallerySessionJson(char *userName) +/* AJAX: add/remove hgsOldSessionName to/from the public gallery (shared 2<->1). Desired state in + * hgsNewSessionShare (0/1). Adding requires a non-empty description, like the classic page. */ +{ +struct sqlConnection *conn = hConnectCentral(); +char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, ""))); +int desired = cgiUsualInt(hgsNewSessionShare, 0); +cartRemove(cart, hgsNewSessionShare); +if (isEmpty(userName)) + { saveSessionJsonError(conn, "Please log in and try again."); return; } +if (isEmpty(sessionName)) + { saveSessionJsonError(conn, "No session was specified."); return; } +char *encUserName = cgiEncodeFull(userName); +char *encSessionName = cgiEncodeFull(sessionName); +boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0); +char query[512]; +if (desired) + { + if (!gotSettings) + { saveSessionJsonError(conn, "This server does not support the public listing."); return; } + sqlSafef(query, sizeof(query), + "select settings from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); + char *settings = sqlQuickString(conn, query); + char *description = getSetting(settings, "description"); + if (isEmpty(description)) + { + saveSessionJsonError(conn, "Please add a description (with the Edit button) before posting " + "this session to the public listing."); + return; + } + } +sqlSafef(query, sizeof(query), "select shared from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +int shared = sqlQuickNum(conn, query); +int newShared = desired ? 2 : 1; +sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d WHERE userName = '%s' AND sessionName = '%s';", + namedSessionTable, newShared, encUserName, encSessionName); +sqlUpdate(conn, query); +sessionTouchLastUse(conn, encUserName, encSessionName); +struct dyString *dyMsg = dyStringNew(256); +if (desired && shared < 2) + thumbnailAdd(encUserName, encSessionName, conn, dyMsg); +if (!desired && shared >= 2) + thumbnailRemove(encUserName, encSessionName, conn); +dyStringFree(&dyMsg); +char extra[32]; +safef(extra, sizeof(extra), ", \"shared\": %d", newShared); +saveSessionJsonOk(conn, extra); +} + +void doOverwriteSessionJson(char *userName) +/* AJAX: re-save the current cart over an existing session (hgsOldSessionName), preserving its + * sharing level. Returns the refreshed created date, view count and assembly. */ +{ +struct sqlConnection *conn = hConnectCentral(); +char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, ""))); +if (isEmpty(userName)) + { saveSessionJsonError(conn, "Please log in and try again."); return; } +if (isEmpty(sessionName)) + { saveSessionJsonError(conn, "No session was specified."); return; } +if (!sqlTableExists(conn, namedSessionTable)) + { saveSessionJsonError(conn, "Required session table does not exist."); return; } +char *encUserName = cgiEncodeFull(userName); +char *encSessionName = cgiEncodeFull(sessionName); +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) + { saveSessionJsonError(conn, "Could not find that session to overwrite."); return; } +sqlSafef(query, sizeof(query), "select shared from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +int shared = sqlQuickNum(conn, query); +int useCount = saveCartAsSession(conn, encUserName, encSessionName, shared); +/* Report the refreshed values so the table row can update in place. */ +boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0); +(void)gotSettings; +sqlSafef(query, sizeof(query), + "select firstUse, contents from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +struct sqlResult *sr = sqlGetResult(conn, query); +char **row = sqlNextRow(sr); +char *dateOnly = NULL, *db2 = NULL; +if (row != NULL) + { + dateOnly = cloneString(row[0]); + char *spacePt = strchr(dateOnly, ' '); + if (spacePt != NULL) + *spacePt = '\0'; + db2 = sessionValFromContents(row[1], "db"); + } +sqlFreeResult(&sr); +struct dyString *extra = dyStringNew(256); +dyStringPrintf(extra, ", \"useCount\": %d", useCount); +if (isNotEmpty(dateOnly)) + dyStringPrintf(extra, ", \"created\": \"%s\"", jsonStringEscape(dateOnly)); +if (isNotEmpty(db2)) + dyStringPrintf(extra, ", \"db\": \"%s\"", jsonStringEscape(db2)); +saveSessionJsonOk(conn, extra->string); +dyStringFree(&extra); +} + +void doDescribeSessionJson(char *userName) +/* AJAX: set the description in the settings ra of hgsOldSessionName (from hgsNewSessionDescription). + * Mirrors the description-editing branch of doSessionChange. */ +{ +struct sqlConnection *conn = hConnectCentral(); +char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, ""))); +char *newDescription = cloneString(cgiUsualString(hgsNewSessionDescription, "")); +cartRemove(cart, hgsNewSessionDescription); +if (isEmpty(userName)) + { saveSessionJsonError(conn, "Please log in and try again."); return; } +if (isEmpty(sessionName)) + { saveSessionJsonError(conn, "No session was specified."); return; } +boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0); +if (!gotSettings) + { saveSessionJsonError(conn, "This server does not support session descriptions."); return; } +char *encUserName = cgiEncodeFull(userName); +char *encSessionName = cgiEncodeFull(sessionName); +char query[512]; +sqlSafef(query, sizeof(query), "select settings from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encSessionName); +char *settings = sqlQuickString(conn, query); +struct hash *settingsHash = raFromString(isEmpty(settings) ? "" : settings); +/* ra syntax needs \n / \r / backslash escaped (kept compatible with doSessionChange). */ +newDescription = replaceChars(newDescription, "\\", "\\\\"); +newDescription = replaceChars(newDescription, "\r", "\\r"); +newDescription = replaceChars(newDescription, "\n", "\\n"); +hashRemove(settingsHash, "description"); +hashAdd(settingsHash, "description", newDescription); +struct dyString *dyRa = dyStringNew(512); +struct hashEl *hel = hashElListHash(settingsHash); +while (hel != NULL) + { + dyStringPrintf(dyRa, "%s %s\n", hel->name, (char *)hel->val); + hel = hel->next; + } +struct dyString *dyQuery = dyStringNew(1024); +sqlDyStringPrintf(dyQuery, "UPDATE %s set settings = '%s' WHERE userName = '%s' AND sessionName = '%s';", + namedSessionTable, dyRa->string, encUserName, encSessionName); +sqlUpdate(conn, dyQuery->string); +dyStringFree(&dyQuery); +dyStringFree(&dyRa); +saveSessionJsonOk(conn, NULL); +} + void hgSession() /* hgSession - Interface with wiki login and do session saving/loading. * Here we set up cart and some global variables, dispatch the command, * and put away the cart when it is done. */ { struct hash *oldVars = hashNew(10); /* Sometimes we output HTML and sometimes plain text; let each outputter * take care of headers instead of using a fixed cart*Shell(). */ cart = cartAndCookieNoContent(hUserCookie(), excludeVars, oldVars); char *userName = (loginSystemEnabled() || wikiLinkEnabled()) ? wikiLinkUserName() : NULL; char *backgroundStatus = cloneString(cartUsualString(cart, "backgroundStatus", NULL)); @@ -2067,30 +2546,50 @@ 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, hgsDoDeleteJson)) + { + doDeleteSessionJson(userName); + } +else if (cartVarExists(cart, hgsDoShareJson)) + { + doShareSessionJson(userName); + } +else if (cartVarExists(cart, hgsDoGalleryJson)) + { + doGallerySessionJson(userName); + } +else if (cartVarExists(cart, hgsDoOverwriteJson)) + { + doOverwriteSessionJson(userName); + } +else if (cartVarExists(cart, hgsDoDescribeJson)) + { + doDescribeSessionJson(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))