fc8de100a3437b9fc33bdeb0f459ca2a93e2f318 max Wed Sep 9 08:14:32 2026 -0700 hgSession: address the code review of the new Sessions page Rename and unshare now keep the public listing's thumbnail with the session it belongs to. The picture's file name is built from the encoded session name, so renaming a listed session left the listing pointing at nothing and the old file behind, and dropping a session from the listing to a plain shared link kept the picture. The classic page had the same problem in a subtler form: it removed the thumbnail after the row had already been renamed, so the old file survived. Saving under a name that is already in use asks before it replaces that session, using the failIfExists reply that the top-right Share a link menu already relies on. The description and "only I can load it" steps that follow a save now report a failure instead of reloading in silence, and what thumbnailAdd has to say when it cannot build a picture reaches the user instead of being freed unread. A session description no longer travels through a title attribute. The tooltip machinery in utils.js inserts its text with innerHTML and an attribute is decoded on the way, so a description containing angle brackets was interpreted as markup rather than shown as typed. It is attached, escaped, after each table draw, which also gives the rows DataTables renders later the same styled mouseovers as the rest of the page. Also: the AJAX endpoints say so when there is no session by that name, instead of reporting a no-op as a success; the new page always offers its way back to the classic page, since the cart variable that got the user there sticks; and four unused CSS rules, a dead element lookup and a dead local are gone. hgConfCatalog cited the wrong ticket for the two sessionNewPage flags. refs #38180, refs #38157 diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c index 3eafb837064..6962005757d 100644 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@ -61,30 +61,36 @@ /* 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); +/* Gallery thumbnail helpers, defined further below with the rest of the gallery code. The AJAX + * endpoints above them have to keep a thumbnail in step with its session, so they need these. */ +int thumbnailAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn, + struct dyString *dyMessage); +void thumbnailRemove(char *encUserName, char *encSessionName, struct sqlConnection *conn); + 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(); @@ -1035,43 +1041,72 @@ "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) + 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". */ + * 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\"}\n", - jsonStringEscape(sessionName), jsonStringEscape(dyUrl->string)); +printf("{\"name\": \"%s\", \"url\": \"%s\"", jsonStringEscape(sessionName), + jsonStringEscape(dyUrl->string)); +if (isNotEmpty(warning)) + printf(", \"warning\": \"%s\"", jsonStringEscape(warning)); +puts("}"); dyStringFree(&dyUrl); hDisconnectCentral(&conn); } +static int sessionSharedLevel(struct sqlConnection *conn, char *encUserName, char *encSessionName) +/* Return the sharing level of this user's session: 0 private, 1 shared by link, 2 in the public + * listing. Returns -1 when the user has no session by that name, which the shared column cannot + * express (it is NOT NULL), so the AJAX endpoints can say so instead of reporting a no-op as a + * success. */ +{ +char query[512]; +sqlSafef(query, sizeof(query), "select shared from %s where userName = '%s' and sessionName = '%s'", + namedSessionTable, encUserName, encSessionName); +char *shared = sqlQuickString(conn, query); +if (shared == NULL) + return -1; +return atoi(shared); +} + +static char *thumbnailWarning(struct dyString *dyMessage) +/* Return what thumbnailAdd had to say for itself, as plain text for a JSON reply, or NULL when it + * said nothing. The message is written for HTML output, so take the <br> back out. */ +{ +if (dyMessage == NULL || dyMessage->stringSize == 0) + return NULL; +return trimSpaces(replaceChars(dyMessage->string, "<br>", " ")); +} + 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"); @@ -1129,31 +1164,31 @@ 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)) snapName = cgiEncodeFull(sessionName); else snapName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName)); saveSnapshotSession(conn, snapshotType, snapUser, snapName, cart); char *snapDecoded = cgiDecodeClone(snapName); - saveSessionJsonResult(conn, snapUser, snapName, snapDecoded); + saveSessionJsonResult(conn, snapUser, snapName, snapDecoded, NULL); return; } char *encUserName = NULL; char *encSessionName = NULL; if (anon) { encUserName = "l"; /* reserved anonymous user -> short link /s/l/<token> */ /* 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. */ if (isEmpty(sessionName)) encSessionName = snapshotNewName(conn, encUserName); else if (startsWith(snapshotNamePrefix, sessionName)) @@ -1181,31 +1216,31 @@ 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); +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, ""))); cartRemove(cart, hgsDoRenameSessionJson); cartRemove(cart, hgsOldSessionName); cartRemove(cart, hgsNewSessionName); @@ -1216,57 +1251,71 @@ 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); + saveSessionJsonResult(conn, encUserName, encNewName, newName, NULL); 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) +int shared = sessionSharedLevel(conn, encUserName, encOldName); +if (shared < 0) { saveSessionJsonError(conn, "Could not find the link to rename."); return; } +/* A gallery thumbnail's file name is built from the encoded session name, so the picture has to be + * moved along with the session or the public listing is left pointing at nothing. Take the old one + * away first, while the row still answers to the old name (the file name also carries firstUse, + * which is read from that row). */ +if (shared >= 2) + thumbnailRemove(encUserName, encOldName, conn); + /* 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); +char *warning = NULL; +if (shared >= 2) + { + struct dyString *dyMessage = dyStringNew(256); + thumbnailAdd(encUserName, encNewName, conn, dyMessage); + warning = thumbnailWarning(dyMessage); + } + +saveSessionJsonResult(conn, encUserName, encNewName, newName, warning); } 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; @@ -1857,55 +1906,56 @@ sr = sqlGetResult(conn, query); if ((row = sqlNextRow(sr)) != NULL) { shared = atoi(row[0]); if (gotSettings) settings = cloneString(row[1]); sqlFreeResult(&sr); } else errAbort("doSessionChange: got no results from query:<BR>\n%s\n", query); char *newName = trimSpaces(cartOptionalString(cart, hgsNewSessionName)); if (isNotEmpty(newName) && !sameString(sessionName, newName)) { char *encNewName = cgiEncodeFull(newName); + // A thumbnail's file name is built from the encoded session name, so take the old picture away + // before the rename, while the row still answers to the old name. + if (shared >= 2) + thumbnailRemove(encUserName, encSessionName, conn); // In case the user has clicked to confirm that they want to overwrite an existing session, // delete the existing row before updating the row that will overwrite it. sqlSafef(query, sizeof(query), "delete from %s where userName = '%s' and sessionName = '%s';", namedSessionTable, encUserName, encNewName); sqlUpdate(conn, query); sqlSafef(query, sizeof(query), "UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';", namedSessionTable, encNewName, encUserName, encSessionName); sqlUpdate(conn, query); dyStringPrintf(dyMessage, "Changed session name from %s to <B>%s</B>.\n", sessionName, newName); sessionName = newName; encSessionName = encNewName; renamePrefixedCartVar(hgsEditPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsLoadPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsDeletePrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsShowDownloadPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsMakeDownloadPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsDoDownloadPrefix , encOldSessionName, encNewName); if (shared >= 2) - { - thumbnailRemove(encUserName, encSessionName, conn); thumbnailAdd(encUserName, encNewName, conn, dyMessage); } - } char sharedVarName[256]; char galleryVarName[256]; safef(sharedVarName, sizeof(sharedVarName), hgsSharePrefix "%s", encOldSessionName); safef(galleryVarName, sizeof(galleryVarName), hgsGalleryPrefix "%s", encOldSessionName); if (cgiBooleanDefined(sharedVarName) || cgiBooleanDefined(galleryVarName)) { int newShared = shared; if (cgiBooleanDefined(sharedVarName)) newShared = cartBoolean(cart, sharedVarName) ? 1 : 0; if (cgiBooleanDefined(galleryVarName)) newShared = cartBoolean(cart, galleryVarName) ? 2 : newShared; if (newShared != shared) { sqlSafef(query, sizeof(query), @@ -2140,35 +2190,38 @@ * 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", + * door. Advertising the new page depends on sessionNewPageBanner (defaulting to sessionNewPage), + * like hgBlat's printNewFormBanner, but the way back off the new page is always printed: the + * sessionNewPage cart variable sticks, so someone who reached the page by typing the variable into + * the URL on a machine where the banner is off would otherwise be stuck there. On the new page + * gbModern.css supplies .gbBanner; the classic page does not load it, so emit a small inline style + * there. */ +{ +if (!onNewPage && !cfgOptionBooleanDefault("sessionNewPageBanner", cfgOptionBooleanDefault("sessionNewPage", FALSE))) return; if (onNewPage) printf("<div class='gbBanner'>You are using the new experimental Sessions page. " "<a href='hgSession?sessionNewPage=0&%s=%s'>Return to the classic page</a>. " "If you have feedback, please let us know at " "<a href='mailto:genome@soe.ucsc.edu'>genome@soe.ucsc.edu</a>.</div>\n", cartSessionVarName(), cartSessionId(cart)); else { printf("<style>.gbBannerClassic{background:#fbf3e2;border:1px solid #d9bd82;padding:10px 14px;" "margin:12px 0;font-size:14px;}</style>\n"); printf("<div class='gbBannerClassic'>We are testing a new Sessions page, with a searchable, " "sortable table and one-click sharing. " "<a href='hgSession?sessionNewPage=1&%s=%s'>Try the new page</a>.</div>\n", @@ -2484,65 +2537,67 @@ 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); +int shared = sessionSharedLevel(conn, encUserName, encSessionName); +if (shared < 0) + { saveSessionJsonError(conn, "Could not find that session."); return; } 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 shared = sessionSharedLevel(conn, encUserName, encSessionName); +if (shared < 0) + { saveSessionJsonError(conn, "Could not find that session."); return; } 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) +/* Either way out of the public listing takes the picture with it: this endpoint drops a session + * from shared==2 to 1 as well as to 0, and the file would otherwise be left behind. */ +if (shared >= 2 && newShared < 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)) @@ -2557,76 +2612,74 @@ { 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 shared = sessionSharedLevel(conn, encUserName, encSessionName); +if (shared < 0) + { saveSessionJsonError(conn, "Could not find that session."); return; } 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); +/* Pass on anything thumbnailAdd had to say, e.g. that this mirror has no ImageMagick convert. The + * session is listed either way, but without this the reply is a bare success and the listing simply + * shows no picture. */ +struct dyString *dyExtra = dyStringNew(64); +dyStringPrintf(dyExtra, ", \"shared\": %d", newShared); +char *warning = thumbnailWarning(dyMsg); +if (warning != NULL) + dyStringPrintf(dyExtra, ", \"warning\": \"%s\"", jsonStringEscape(warning)); +saveSessionJsonOk(conn, dyExtra->string); } 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) +int shared = sessionSharedLevel(conn, encUserName, encSessionName); +if (shared < 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); @@ -2646,30 +2699,32 @@ { 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]; +if (sessionSharedLevel(conn, encUserName, encSessionName) < 0) + { saveSessionJsonError(conn, "Could not find that session."); return; } 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);