ee89e6768015d512b5ca8db8a62e8a129dfd5c7f
braney
  Sun Aug 23 15:56:57 2026 -0700
hgSession, cartReset: send the content policy header on their own pages

Both CGIs write their own http header block, so neither reaches addHttpHeaders
in cart.c, the hook that carries the policy header.  hgSession lets each output
path write its own Content-Type, and cartReset gets its header from
htmShellWithHead.

Add the call ahead of each html Content-Type: the two main page functions in
hgSession.c, three more in backup.c, and the one in cartReset.  The json paths
and the tar download are left alone, since they print no html.

With the hg.conf option off this changes nothing.  With it on, both pages now
carry the header, and its nonce matches the one in the meta tag and on the
inline script tags.

diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index e1230acd091..c63fcc0bd8e 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -1,2649 +1,2651 @@
 /* hgSession - Interface with wiki login and do session saving/loading. */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 /* WARNING: testing this CGI on hgwbeta can lead to missed bugs. This is
  * because on hgwbeta, the login links go just to hgwbeta. But on genome-euro
  * and genome-asia, the login links go to genome.ucsc.edu and session links to 
  * genome-euro/asia. For proper testing of session loading on hgSession and
  * hgLogin, configure your sandbox to use genome.ucsc.edu as a "remote login"
  * (wiki.host=genome.ucsc.edu). Make sure that links to load sessions go to
  * your sandbox, but login links go to genome.ucsc.edu. For QA, tell them to
  * use genome-preview to test this CGI. */
 
 #include "common.h"
 #include "hash.h"
 #include "htmshell.h"
 #include "cheapcgi.h"
 #include "linefile.h"
 #include "net.h"
 #include "textOut.h"
 #include "hCommon.h"
 #include "hui.h"
 #include "cart.h"
 #include "jsHelper.h"
 #include "web.h"
 #include "hdb.h"
 #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"
 #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 <various CGI settings>\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();
 
 cartWebStart(cart, NULL, "Welcome %s", wikiUserName);
 jsInit();
 
 jsIncludeDataTablesLibs();
 
 if (loginSystemEnabled()) /* Using the new hgLogin CGI for login */
     {
     printf("<h4 style=\"margin: 0pt 0pt 7px;\">Your Account Information</h4>"
         "<ul style=\"list-style: none outside none; margin: 0pt; padding: 0pt;\">"
         "<li>Username:  %s</li>",wikiUserName);
 
     if (loginUseBasicAuth())
         printf("<li>The Genome Browser is configured to use HTTP Basic Authentication, so the password cannot be changed here.</li>");
     else
         printf("<li><A HREF=\"%s\">Change password</A></li>",
             wikiLinkChangePasswordUrl(cartSessionId(cart)));
     char *changeEmailUrl = wikiLinkChangeEmailUrl(cartSessionId(cart));
     if (changeEmailUrl != NULL)
         printf("<li><A HREF=\"%s\">Change email</A></li>", changeEmailUrl);
     printf("</ul>");
 
     printf("<p><A id='logoutLink' HREF=\"%s\">Sign out</A></p>",
         wikiLinkUserLogoutUrl(cartSessionId(cart)));
 
     if (loginUseBasicAuth())
             wikiFixLogoutLinkWithJs();
     }
 
 else
 /* this part is not used anymore at UCSC since 2014 */
     {
     printf("If you are not %s (on the wiki at "
         "<A HREF=\"http://%s/\" TARGET=_BLANK>%s</A>) "
         "and would like to sign out or change identity, \n",
         wikiUserName, wikiHost, wikiHost);
     printf("<A HREF=\"%s\"><B>click here to sign out.</B></A>\n",
         wikiLinkUserLogoutUrl(cartSessionId(cart)));
     }
 }
 
 void offerLogin()
 /* Tell the user they are not logged in to the system and tell them how to
  * do so. */
 {
 char *wikiHost = wikiLinkHost();
 
 cartWebStart(cart, NULL, "Sign in to UCSC Genome Bioinformatics");
 jsInit();
 if (loginSystemEnabled())
     {
    printf("<ul style=\"list-style: none outside none; margin: 0pt; padding: 0pt;\">"
 "<li><A HREF=\"%s\">Login</A></li>",
         wikiLinkUserLoginUrl(cartSessionId(cart)));
     printf("<li><A HREF=\"%s\">"
         "Create an account</A></li></ul>",
         wikiLinkUserSignupUrl(cartSessionId(cart)));
     printf("<P>Signing in enables you to save current settings into a "
         "named session, and then restore settings from the session later. <BR>"
         "If you wish, you can share named sessions with other users.</P>");
     }    
 else
     // the following block is not used at UCSC anymore since 2014
     {
     printf("Signing in enables you to save current settings into a "
         "named session, and then restore settings from the session later.\n"
         "If you wish, you can share named sessions with other users.\n");
     printf("<P>The sign-in page is handled by our "
         "<A HREF=\"http://%s/\" TARGET=_BLANK>wiki system</A>:\n", wikiHost);
     printf("<A HREF=\"%s\"><B>click here to sign in.</B></A>\n",
         wikiLinkUserLoginUrl(cartSessionId(cart)));
     printf("The wiki also serves as a forum for users "
         "to share knowledge and ideas.\n");
     }
 }
 
 
 void showCartLinks()
 /* Print out links to cartDump and cartReset. */
 {
 char *session = cartSidUrlString(cart);
 char returnAddress[512];
 
 safef(returnAddress, sizeof(returnAddress), "%s?%s", hgSessionName(), session);
 printf("<A HREF=\"../cgi-bin/cartReset?%s&destination=%s\">Click here to "
        "reset</A> the browser user interface settings to their defaults.\n",
        session, cgiEncodeFull(returnAddress));
 }
 
 void addSessionLink(struct dyString *dy, char *userName, char *sessionName,
 		    boolean encode, boolean tryShortLink)
 /* Add to dy an URL that tells hgSession to load a saved session.
  * If encode, cgiEncodeFull the URL. 
  * If tryShortLink, print a shortened link that apache can redirect.
  * The link is an absolute link that includes the server name so people can
  * copy-paste it into emails.
  *
  * NOTE: Do not append CGI variables here, as it will break short links if they are enabled. */
 {
 struct dyString *dyTmp = dyStringNew(1024);
 if (tryShortLink && cfgOptionBooleanDefault("hgSession.shortLink", FALSE) &&
         !stringIn("%2F", userName) && !stringIn("%2F", sessionName))
     dyStringPrintf(dyTmp, "http%s://%s/s/%s/%s", cgiAppendSForHttps(), cgiServerNamePort(),
         userName, sessionName);
 else
     dyStringPrintf(dyTmp, "%shgTracks?hgS_doOtherUser=submit&"
 	       "hgS_otherUserName=%s&hgS_otherUserSessionName=%s",
 	       hLocalHostCgiBinUrl(), userName, sessionName);
 if (encode)
     {
     dyStringPrintf(dy, "%s", cgiEncodeFull(dyTmp->string));
     }
 else
     {
     dyStringPrintf(dy, "%s", dyTmp->string);
     }
 dyStringFree(&dyTmp);
 }
 
 void printCopyToClipboardButton(struct dyString *dy, char *iconId, char *targetId, char *buttonLabel) 
 /* print a copy-to-clipboard button with DOM id iconId that copies the node text of targetId */
 {
 dyStringPrintf(dy, "&nbsp;<button title='Copy URL to clipboard' id='%s' data-target='%s'><svg style='width:0.9em' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d='M502.6 70.63l-61.25-61.25C435.4 3.371 427.2 0 418.7 0H255.1c-35.35 0-64 28.66-64 64l.0195 256C192 355.4 220.7 384 256 384h192c35.2 0 64-28.8 64-64V93.25C512 84.77 508.6 76.63 502.6 70.63zM464 320c0 8.836-7.164 16-16 16H255.1c-8.838 0-16-7.164-16-16L239.1 64.13c0-8.836 7.164-16 16-16h128L384 96c0 17.67 14.33 32 32 32h47.1V320zM272 448c0 8.836-7.164 16-16 16H63.1c-8.838 0-16-7.164-16-16L47.98 192.1c0-8.836 7.164-16 16-16H160V128H63.99c-35.35 0-64 28.65-64 64l.0098 256C.002 483.3 28.66 512 64 512h192c35.2 0 64-28.8 64-64v-32h-47.1L272 448z'/></svg>%s</button>\n", iconId, targetId, buttonLabel);
 
 jsOnEventById("click", iconId, "copyToClipboard(event);");
 }
 
 void printShareMessage(struct dyString *dy, char *userName, char *sessionName,
             boolean encode)
 {
 struct dyString *dyTmp = dyStringNew(0);
 addSessionLink(dyTmp, userName, sessionName, encode, TRUE);
 dyStringPrintf(dy,
     "<p>You can share this session with the following URL:<br><span id='urlText'>%s</span>&nbsp;",
     dyTmp->string);
 
 printCopyToClipboardButton(dy, "copyIcon", "urlText", "&nbsp;Copy to clipboard");
 dyStringAppend(dy, "</p>");
 }
 
 char *getSessionLink(char *encUserName, char *encSessionName)
 /* Form a link that will take the user to a bookmarkable page that
  * will load the given session. */
 {
 struct dyString *dy = dyStringNew(1024);
 dyStringPrintf(dy, "<A HREF=\"");
 addSessionLink(dy, encUserName, encSessionName, FALSE, TRUE);
 dyStringPrintf(dy, "\">Browser</A>\n");
 return dyStringCannibalize(&dy);
 }
 
 char *getSessionEmailLink(char *encUserName, char *encSessionName)
 /* Invoke mailto: with a cgi-encoded link that will take the user to a
  * bookmarkable page that will load the given session. */
 {
 struct dyString *dy = dyStringNew(1024);
 dyStringPrintf(dy, "<A HREF=\"mailto:?subject=UCSC browser session %s&"
 	       "body=Here is a UCSC browser session I%%27d like to share with "
 	       "you:%%20",
 	       cgiDecodeClone(encSessionName));
 addSessionLink(dy, encUserName, encSessionName, TRUE, TRUE);
 dyStringPrintf(dy, "\">Email</A>\n");
 return dyStringCannibalize(&dy);
 }
 
 void addUrlLink(struct dyString *dy, char *url, boolean encode)
 /* Add to dy an URL that tells hgSession to load settings from the given url.
  * If encode, cgiEncodeFull the whole thing. */
 {
 struct dyString *dyTmp = dyStringNew(1024);
 char *encodedUrl = cgiEncodeFull(url);
 dyStringPrintf(dyTmp, "%shgTracks?hgS_doLoadUrl=submit&hgS_loadUrlName=%s",
 	       hLocalHostCgiBinUrl(), encodedUrl);
 if (encode)
     {
     dyStringPrintf(dy, "%s", cgiEncodeFull(dyTmp->string));
     }
 else
     {
     dyStringPrintf(dy, "%s", dyTmp->string);
     }
 freeMem(encodedUrl);
 dyStringFree(&dyTmp);
 }
 
 char *getUrlLink(char *url)
 /* Form a link that will take the user to a bookmarkable page that
  * will load the given url. */
 {
 struct dyString *dy = dyStringNew(1024);
 dyStringPrintf(dy, "<A HREF=\"");
 addUrlLink(dy, url, FALSE);
 dyStringPrintf(dy, "\">Browser</A>\n");
 return dyStringCannibalize(&dy);
 }
 
 char *getUrlEmailLink(char *url)
 /* Invoke mailto: with a cgi-encoded link that will take the user to a
  * bookmarkable page that will load the given url. */
 {
 struct dyString *dy = dyStringNew(1024);
 dyStringPrintf(dy, "<A HREF=\"mailto:?subject=UCSC browser session&"
 	       "body=Here is a UCSC browser session I%%27d like to share with "
 	       "you:%%20");
 addUrlLink(dy, url, TRUE);
 dyStringPrintf(dy, "\">Email</A>\n");
 return dyStringCannibalize(&dy);
 }
 
 static char *getSetting(char *settings, char *name)
 /* Dig out one setting from a settings string that we're only going to
  * look at once (so we don't keep the hash around). */
 {
 if (isEmpty(settings))
     return NULL;
 struct hash *settingsHash = raFromString(settings);
 char *val = cloneString(hashFindVal(settingsHash, name));
 hashFree(&settingsHash);
 return val;
 }
 
 static struct slName *showExistingSessions(char *userName)
 /* Print out a table with buttons for sharing/unsharing/loading/deleting
  * previously saved sessions.  Return a list of session names. */
 {
 struct slName *existingSessionNames = NULL;
 struct sqlConnection *conn = hConnectCentral();
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char query[512];
 boolean foundAny = FALSE;
 char *encUserName = cgiEncodeFull(userName);
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
 /* DataTables configuration: only allow ordering on session name, creation date, and database.
  * https://datatables.net/reference/option/columnDefs */
 jsInlineF(
         "if (theClient.isIePre11() === false)\n{\n"
         "$(document).ready(function () {\n"
         "    $('#sessionTable').DataTable({\"columnDefs\": [{\"orderable\":false, \"targets\":[0,4,5,6,7,8]}],\n"
         "                                       \"order\":[2,'desc'],\n"
         "                                       \"stateSave\":true,\n"
         "                                       \"stateSaveCallback\": %s,\n"
         "                                       \"stateLoadCallback\": %s\n"
         "                                 });\n"
         "} );\n"
         "}\n"
         , jsDataTableStateSave(hgSessionPrefix), jsDataTableStateLoad(hgSessionPrefix, cart));
 
 printf("<style>#sessionTable_filter { float:left !important; margin-left:20px; }</style>\n");
 printf("<H3>My Sessions</H3>\n");
 printf("<table id=\"sessionTable\" class=\"sessionTable stripe hover row-border compact\" borderwidth=0>\n");
 printf("<thead><tr>");
 printf("<th></th>"
        "<th style=\"white-space:nowrap;text-align:left\">Session name (click to load)</th>"
        "<th style=\"text-align:left\">Created on</th><th style=\"text-align:left\">View count</th>"
        "<th style=\"text-align:left\">Assembly</th>"
        "<th style=\"text-align:left\">View/edit&nbsp;<BR>details&nbsp;</th>"
        "<th style=\"text-align:left\">Delete this&nbsp;<BR>session&nbsp;</th>"
        "<th style=\"text-align:left\">Share with&nbsp;<BR>others?&nbsp;</th>"
        "<th style=\"text-align:left\">Post in&nbsp;<br><a href=\"../cgi-bin/hgPublicSessions?%s\">public listing</a>?</th>"
        "<th style=\"text-align:left\">Send to<BR>mail</th>",
        cartSidUrlString(cart));
 printf("</tr></thead>");
 printf("<tbody>\n");
 
 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];
     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("<TR><TD>&nbsp;&nbsp;</TD><TD>");
 
     char iconId[256];
     char linkId[256];
     safef(linkId, sizeof(linkId), "linkEl-%d", rowIdx);
     safef(iconId, sizeof(iconId), "iconEl-%d", rowIdx);
     struct dyString *buttonText = dyStringNew(4096);
     printCopyToClipboardButton(buttonText, iconId, linkId, "");
     puts(dyStringCannibalize(&buttonText));
     puts("&nbsp;");
 
     struct dyString *dy = dyStringNew(1024);
     addSessionLink(dy, encUserName, encSessionName, FALSE, TRUE);
     char *sessionUrl = dyStringContents(dy);
     printf("<a id='linkEl-%d' data-copy='%s' href=\"%s\">%s</a>", rowIdx, sessionUrl, sessionUrl, htmlEncode(sessionName));
     dyStringFree(&dy);
     rowIdx++;
 
     struct tm firstUseTm;
     ZeroVar(&firstUseTm);
     strptime(firstUse, "%Y-%m-%d %T", &firstUseTm);
     char *spacePt = strchr(firstUse, ' ');
     if (spacePt != NULL) *spacePt = '\0';
 
     char *useCount = row[3];
     printf("&nbsp;&nbsp;</TD>"
             "<TD data-order=\"%ld\"><nobr>%s</nobr>&nbsp;&nbsp;</TD>"
             "<TD>%s</TD>"
             "<TD align='left'>", mktime(&firstUseTm), firstUse, useCount);
 
     char *dbIdx = NULL;
     if (startsWith("db=", row[4]))
         dbIdx = row[4]+3;
     else
         dbIdx = strstr(row[4], "&db=") + 4;
         
     if (dbIdx != NULL)
         {
         char *dbEnd = strchr(dbIdx, '&');
         char *db = NULL;
         if (dbEnd != NULL)
             db = cloneStringZ(dbIdx, dbEnd-dbIdx);
         else
             db = cloneString(dbIdx);
         printf("%s</td><td align='center'>", db);
         }
     else
         printf("n/a</td><td align=center>");
 
     if (gotSettings)
         {
         safef(buf, sizeof(buf), "%s%s", hgsEditPrefix, encSessionName);
         cgiMakeButton(buf, "View/edit");
         char *description = getSetting(row[5], "description");
         if (!isEmpty(description))
             hasDescription = TRUE;
         }
     else
         printf("unavailable");
 
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsDeletePrefix, encSessionName);
     char command[512];
     safef(command, sizeof(command), confirmDeleteFormat, encSessionName);
     cgiMakeOnClickSubmitButton(command, buf, "Delete");
 
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsSharePrefix, encSessionName);
     cgiMakeCheckBoxWithId(buf, shared>0, buf);
     jsOnEventById("change",buf,"document.mainForm.submit();");
 
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsGalleryPrefix, encSessionName);
     cgiMakeCheckBoxFourWay(buf, inGallery, shared>0, buf, NULL, NULL);
     if (hasDescription || inGallery)
         jsOnEventById("change", buf, "document.mainForm.submit();");
     else
         jsOnEventById("change", buf, "warn('Please first use the view/edit option to "
                 "add a description for this session.'); this.checked = false;");
 
     link = getSessionEmailLink(encUserName, encSessionName);
     printf("</td><td align=center>%s</td></tr>", link);
     freez(&link);
     foundAny = TRUE;
     struct slName *sn = slNameNew(sessionName);
     slAddHead(&existingSessionNames, sn);
     }
 if (!foundAny)
     printf("<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>(none)</TD>"
 	   "<TD colspan=5></TD></TR>\n");
 
 printf("</tbody>\n");
 
 printf("</TABLE>\n");
 printf("<P></P>\n");
 sqlFreeResult(&sr);
 hDisconnectCentral(&conn);
 return existingSessionNames;
 }
 
 void showOtherUserOptions()
 /* Print out inputs for loading another user's saved session. */
 {
 printf("<TABLE BORDERWIDTH=0>\n");
 printf("<TR><TD colspan=2>"
        "Use settings from another user's saved session:</TD></TR>\n"
        "<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>User: \n");
 cgiMakeOnKeypressTextVar(hgsOtherUserName,
 			 cartUsualString(cart, hgsOtherUserName, ""),
 			 20, "return noSubmitOnEnter(event);");
 printf("&nbsp;&nbsp;&nbsp;Session name: \n");
 cgiMakeOnKeypressTextVar(hgsOtherUserSessionName,
 			 cartUsualString(cart, hgsOtherUserSessionName, ""),
 			 20, jsPressOnEnter(hgsDoOtherUser));
 printf("&nbsp;&nbsp;");
 cgiMakeButton(hgsDoOtherUser, "Submit");
 printf("</TD></TR>\n");
 printf("<TR><TD colspan=2></TD></TR>\n");
 printf("</TABLE>\n");
 }
 
 void showLoadingOptions(char *userName, boolean savedSessionsSupported)
 /* Show options for loading settings from another user's session, a file
  * or URL. */
 {
 printf("<H3>Restore Settings</H3>\n");
 if (savedSessionsSupported)
     showOtherUserOptions();
 
 printf("<TABLE BORDERWIDTH=0>\n");
 printf("<TR><TD colspan=2>Use settings from a local file:</TD>\n");
 printf("<TD><INPUT TYPE=FILE NAME=\"%s\" id='%s'>\n", hgsLoadLocalFileName,  hgsLoadLocalFileName);
 jsOnEventById("keypress", hgsLoadLocalFileName,"return noSubmitOnEnter(event);");
 printf("&nbsp;&nbsp;");
 cgiMakeButton(hgsDoLoadLocal, "Submit");
 printf("</TD></TR>\n");
 printf("<TR><TD colspan=2></TD></TR>\n");
 
 printf("<TR><TD colspan=2>Use settings from a URL (http://..., ftp://...):"
        "</TD>\n");
 printf("<TD>\n");
 cgiMakeOnKeypressTextVar(hgsLoadUrlName,
 			 cartUsualString(cart, hgsLoadUrlName, ""),
 			 20, jsPressOnEnter(hgsDoLoadUrl));
 printf("&nbsp;&nbsp;");
 cgiMakeButton(hgsDoLoadUrl, "Submit");
 printf("</TD></TR>\n");
 
 printf("</TABLE>\n");
 
 printf("<P></P>\n");
 printf("Please note: the above URL option is <em>not</em> for loading track hubs or assembly hubs.\n");
 printf("To load those data resources into the browser, please visit the <a href=\"../cgi-bin/hgHubConnect\"\n");
 printf("target=\"_blank\">Track Hubs</a> listing page, click the \"Connected Hubs\" tab, and enter the hub URL there.\n");
 
 printf("<P></P>\n");
 }
 
 static struct dyString *dyPrintCheckExistingSessionJs(struct slName *existingSessionNames,
                                                       char *exceptName)
 /* Write JS that will pop up a confirm dialog if the user's new session name is the same
  * (case-insensitive) as any existing session name, i.e. they would be overwriting it.
  * If exceptName is given, then it's OK for the new session name to match that. */
 {
 struct dyString *js = dyStringNew(1024);
 struct slName *sn;
 // MySQL does case-insensitive comparison because our DEFAULT CHARSET=latin1;
 // use case-insensitive comparison here to avoid clobbering (#15051).
 dyStringAppend(js, "var su, si = document.getElementsByName('" hgsNewSessionName "'); ");
 dyStringAppend(js, "if (si[0]) { su = si[0].value.trim().toUpperCase(); ");
 if (isNotEmpty(exceptName))
     dyStringPrintf(js, "if (su !== '%s'.toUpperCase()) { ", exceptName);
 dyStringAppend(js, "if ( ");
 for (sn = existingSessionNames;  sn != NULL;  sn = sn->next)
     {
     char nameUpper[PATH_LEN];
     safecpy(nameUpper, sizeof(nameUpper), sn->name);
     touppers(nameUpper);
     dyStringPrintf(js, "su === ");
     dyStringQuoteString(js, '\'', nameUpper);
     dyStringPrintf(js, "%s", (sn->next ? " || " : " )"));
     }
 dyStringAppend(js, " { return confirm('This will overwrite the contents of the existing "
                "session ' + si[0].value.trim() + '.  Proceed?'); } }");
 if (isNotEmpty(exceptName))
     dyStringAppend(js, " }");
 return js;
 }
 
 void showSavingOptions(char *userName, struct slName *existingSessionNames)
 /* Show options for saving a new named session in our db or to a file. */
 {
 printf("<H3>Save Settings</H3>\n");
 printf("<TABLE BORDERWIDTH=0>\n");
 
 if (isNotEmpty(userName))
     {
     printf("<TR><TD colspan=4>Save current settings as named session:"
 	   "</TD></TR>\n"
 	   "<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>Name:</TD><TD>\n");
     cgiMakeOnKeypressTextVar(hgsNewSessionName,
 			     hubConnectSkipHubPrefix(cartUsualString(cart, "db", "mySession")),
 			     20, jsPressOnEnter(hgsDoNewSession));
     printf("&nbsp;&nbsp;&nbsp;");
     cgiMakeCheckBox(hgsNewSessionShare,
 		    cartUsualBoolean(cart, hgsNewSessionShare, TRUE));
     printf("Allow this session to be loaded by others\n");
     printf("</TD><TD>");
     printf("&nbsp;");
     if (existingSessionNames)
 	{
         struct dyString *js = dyPrintCheckExistingSessionJs(existingSessionNames, NULL);
 	cgiMakeOnClickSubmitButton(js->string, hgsDoNewSession, "Submit");
 	dyStringFree(&js);
 	}
     else
 	cgiMakeButton(hgsDoNewSession, "submit");
     printf("</TD></TR>\n");
     printf("<TR><TD colspan=4></TD></TR>\n");
     }
 
 printf("<TR><TD colspan=4>Save current settings to a local file:</TD></TR>\n");
 printf("<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>File:</TD><TD>\n");
 cgiMakeOnKeypressTextVar(hgsSaveLocalFileName,
 			 cartUsualString(cart, hgsSaveLocalFileName, ""),
 			 20, jsPressOnEnter(hgsDoSaveLocal));
 printf("&nbsp;&nbsp;&nbsp;");
 printf("File type returned: ");
 char *compressType = cartUsualString(cart, hgsSaveLocalFileCompress, textOutCompressNone);
 cgiMakeRadioButton(hgsSaveLocalFileCompress, textOutCompressNone,
 		   differentWord(textOutCompressGzip, compressType));
 printf("&nbsp;plain text&nbsp;&nbsp;");
 cgiMakeRadioButton(hgsSaveLocalFileCompress, textOutCompressGzip,
 		   sameWord(textOutCompressGzip, compressType));
 printf("&nbsp;gzip compressed (ignored if output file is blank)");
 printf("</TD><TD>");
 printf("&nbsp;");
 cgiMakeButton(hgsDoSaveLocal, "Submit");
 printf("</TD></TR>\n");
 printf("<TR><TD></TD><TD colspan=3>(leave file blank to get output in "
        "browser window)</TD></TR>\n");
 printf("<TR><TD colspan=4></TD></TR>\n");
 
 printf("<TR><TD colspan=4>Save Custom Tracks:</TD></TR>\n");
 printf("<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD colspan=2>");
 printf("Back up custom tracks to archive .tar.gz</TD>");
 printf("<TD>");
 printf("&nbsp;");
 cgiMakeButton(hgsShowDownloadPrefix, "Submit");
 printf("</TD></TR>\n");
 
 printf("<TR><TD colspan=4></TD></TR>\n");
 printf("</TABLE>\n");
 }
 
 void showSessionControls(char *userName, boolean savedSessionsSupported,
 			 boolean webStarted)
 /* 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("<P>See the <A HREF=\"../goldenPath/help/hgSessionHelp.html\" "
        "TARGET=_BLANK>Sessions User's Guide</A> "
        "for more information about this tool. "
        "See the <A HREF=\"../goldenPath/help/sessions.html\" "
        "TARGET=_BLANK>Session Gallery</A> "
        "for example sessions.</P>\n");
 
 showCartLinks();
 
 printf("<FORM ACTION=\"%s\" NAME=\"mainForm\" METHOD=%s "
        "ENCTYPE=\"multipart/form-data\">\n",
        hgSessionName(), formMethod);
 cartSaveSession(cart);
 
 struct slName *existingSessionNames = NULL;
 if (isNotEmpty(userName))
     existingSessionNames = showExistingSessions(userName);
 else if (savedSessionsSupported)
      printf("<P>If you <A HREF=\"%s\">sign in</A>, "
          "you will also have the option to save named sessions.</P>\n",
          wikiLinkUserLoginUrl(cartSessionId(cart)));
 showSavingOptions(userName, existingSessionNames);
 showLoadingOptions(userName, savedSessionsSupported);
 printf("</FORM>\n");
 }
 
 void showLinkingTemplates(char *userName)
 /* Explain how to create links to us for sharing sessions. */
 {
 struct dyString *dyUrl = dyStringNew(1024);
 webNewSection("Sharing Sessions");
 printf("There are several ways to share saved sessions with others.\n");
 printf("<UL>\n");
 if (userName != NULL)
     {
     printf("<LI>Each previously saved named session appears with "
 	   "Browser and Email links.  "
            "The Email link invokes your email tool with a message "
            "containing the Genome Browser link. The Email link can "
            "be bookmarked in your web browser and/or shared with "
            "others. If you right-click and copy the Browser link, "
            "it will be the same as the Email link. However, if you "
            "click the Browser link it will take you to the Genome "
            "Browser and become a uniquely identified URL once the "
            "session loads, so that resulting link is not advised "
            "for sharing.</LI>\n"
        "<li>Each previously saved named session also appears with "
            "a checkbox to add the session to our "
            "<a href=\"../cgi-bin/hgPublicSessions?%s\">Public Sessions</a> "
            "listing. Adding a session to this listing allows other "
            "browser users to view the description and a thumbnail "
            "image of your session, and to load the session if they "
            "are interested.</li>\n", cartSidUrlString(cart));
     }
 else if (loginSystemEnabled() || wikiLinkEnabled())
     {
      printf("<LI>If you <A HREF=\"%s\">sign in</A>, you will be able " 
             " to save named sessions which will be displayed with "
             " Browser and Email links.</LI>\n",
             wikiLinkUserLoginUrl(cartSessionId(cart)));
     }
 dyStringPrintf(dyUrl, "%shgTracks", hLocalHostCgiBinUrl());
 
 printf("<LI>If you have saved your settings to a local file, you can send "
        "email to others with the file as an attachment and direct them to "
        "<A HREF=\"%s\">%s</A> .</LI>\n",
        dyUrl->string, dyUrl->string);
 dyStringPrintf(dyUrl, "?hgS_doLoadUrl=submit&hgS_loadUrlName=");
 printf("<LI>If a saved settings file is available from a web server, "
        "you can send email to others with a link such as "
        "%s<B>U</B> where <B>U</B> 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 <A HREF=\"../goldenPath/help/sessions.html\" "
        "TARGET=_BLANK>Session Gallery</A>.</LI>\n",
        dyUrl->string);
 printf("</UL>\n");
 dyStringFree(&dyUrl);
 }
 
 void doMainPage(char *userName, char *message)
 /* Login status/links and session controls. */
 {
 if (sessionNewPageActive())
     {
     doMainPageNew(userName, message);
     return;
     }
+cspWriteResponseHeader();
 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);
 	}
     showSessionControls(userName, TRUE, TRUE);
     showLinkingTemplates(userName);
     }
 else 
     {
     if (isNotEmpty(message))
 	{
 	if (cartVarExists(cart, hgsDoSessionDetail))
 	    webNewSection("Session Details");
 	else
 	    cartWebStart(cart, NULL, "Updated Session");
 	jsInit();
 	puts(message);
 	showSessionControls(NULL, FALSE, TRUE);
 	}
     else
 	showSessionControls(NULL, FALSE, FALSE);
     showLinkingTemplates(NULL);
     }
 cartWebEnd();
 }
 
 
 void cleanHgSessionFromCart(struct cart *cart)
 /* Remove hgSession action variables that should not stay in the cart. */
 {
 char varName[256];
 safef(varName, sizeof(varName), "%s%s", cgiBooleanShadowPrefix(), hgsSharePrefix);
 cartRemovePrefix(cart, varName);
 cartRemovePrefix(cart, hgsSharePrefix);
 safef(varName, sizeof(varName), "%s%s", cgiBooleanShadowPrefix(), hgsGalleryPrefix);
 cartRemovePrefix(cart, varName);
 cartRemovePrefix(cart, hgsGalleryPrefix);
 cartRemovePrefix(cart, hgsLoadPrefix);
 cartRemovePrefix(cart, hgsEditPrefix);
 cartRemovePrefix(cart, hgsLoadLocalFileName);
 cartRemovePrefix(cart, hgsDeletePrefix);
 cartRemovePrefix(cart, hgsShowDownloadPrefix);
 cartRemovePrefix(cart, hgsMakeDownloadPrefix);
 cartRemovePrefix(cart, hgsDoDownloadPrefix);
 cartRemovePrefix(cart, hgsDo);
 cartRemove(cart, hgsOldSessionName);
 cartRemove(cart, hgsCancel);
 }
 
 static void outIfNotPresent(struct cart *cart, struct dyString *dy, char *track, int tdbVis)
 /* Output default trackDb visibility if it's not mentioned in the cart. */
 {
 char *cartVis = cartOptionalString(cart, track);
 if (cartVis == NULL)
     {
     if (dy)
         dyStringPrintf(dy,"&%s=%s", track, hStringFromTv(tdbVis));
     else
         printf("%s %s\n", track, hStringFromTv(tdbVis));
     }
 }
 
 static void outAttachedHubUrls(struct cart *cart, struct dyString *dy)
 /* output the hubUrls for all attached hubs in the cart. */
 {
 struct hubConnectStatus *statusList = hubConnectStatusListFromCart(cart, NULL);
 
 if (statusList == NULL)
     return;
 
 if (dy)
     dyStringPrintf(dy,"&assumesHub=");
 else
     printf("assumesHub ");
 for(; statusList; statusList = statusList->next)
     {
     if (dy)
         dyStringPrintf(dy,"%d=%s ", statusList->id, cgiEncode(statusList->hubUrl));
     else
         printf("%d=%s ", statusList->id, statusList->hubUrl);
     }
 if (dy == NULL)
     printf("\n");
 }
 
 static void outDefaultTracks(struct cart *cart, struct dyString *dy)
 /* Output the default trackDb visibility for all tracks
  * in trackDb if the track is not mentioned in the cart. */
 {
 database = cartString(cart, "db");
 struct trackDb *tdb = NULL;
 // Some old sessions reference databases that are no longer present, and that triggers an errAbort
 // when calling hgTrackDb.  Just move on instead of errAborting.
 struct errCatch *errCatch = errCatchNew();
 if (errCatchStart(errCatch))
     tdb = hTrackDb(database);
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     {
     fprintf(stderr, "outDefaultTracks: Error from hTrackDb: '%s'; Continuing...",
             errCatch->message->string);
     tdb = NULL;
     }
 errCatchFree(&errCatch);
 
 struct hash *parentHash = newHash(5);
 
 for(; tdb; tdb = tdb->next)
     {
     struct trackDb *parent = tdb->parent;
     if (parent) 
         {
         if (hashLookup(parentHash, parent->track) == NULL)
             {
             hashStore(parentHash, parent->track);
             if (parent->isShow)
                 outIfNotPresent(cart, dy, parent->track, tvShow);
             }
         }
     if (tdb->visibility != tvHide)
         outIfNotPresent(cart, dy, tdb->track, tdb->visibility);
     }
 
 // Put a variable in the cart that says we put the default 
 // visibilities in it.
 if (dy)
     dyStringPrintf(dy,"&%s=on", CART_HAS_DEFAULT_VISIBILITY);
 else
     printf("%s on", CART_HAS_DEFAULT_VISIBILITY);
 }
 
 #define INITIAL_USE_COUNT 0
 static int saveCartAsSession(struct sqlConnection *conn, char *encUserName, char *encSessionName,
                              int sharingLevel)
 /* Save all settings in cart, either adding a new session or overwriting an existing session.
  * Return useCount so that the caller can distinguish between adding and overWriting. */
 {
 struct sqlResult *sr = NULL;
 struct dyString *dy = dyStringNew(16 * 1024);
 char **row;
 char *firstUse = NULL;
 int useCount = INITIAL_USE_COUNT;
 char *settings = "";
 
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
 /* If this session already existed, preserve its firstUse, useCount,
  * and settings (if available). */
 if (gotSettings)
     sqlDyStringPrintf(dy, "SELECT firstUse, useCount, settings FROM %s "
                   "WHERE userName = '%s' AND sessionName = '%s';",
                   namedSessionTable, encUserName, encSessionName);
 else
     sqlDyStringPrintf(dy, "SELECT firstUse, useCount FROM %s "
                   "WHERE userName = '%s' AND sessionName = '%s';",
                   namedSessionTable, encUserName, encSessionName);
 sr = sqlGetResult(conn, dy->string);
 if ((row = sqlNextRow(sr)) != NULL)
     {
     firstUse = cloneString(row[0]);
     useCount = atoi(row[1]) + 1;
     if (gotSettings)
         {
         settings = cloneString(row[2]);
         if (settings == NULL)
             settings = "";
         }
     }
 sqlFreeResult(&sr);
 
 sessionDataSaveSession(cart, encUserName, encSessionName, cgiOptionalString(hgsSessionDataDbSuffix));
 
 /* Remove pre-existing session (if any) before updating. */
 dyStringClear(dy);
 sqlDyStringPrintf(dy, "DELETE FROM %s WHERE userName = '%s' AND "
                   "sessionName = '%s';",
                   namedSessionTable, encUserName, encSessionName);
 sqlUpdate(conn, dy->string);
 
 dyStringClear(dy);
 sqlDyStringPrintf(dy, "INSERT INTO %s ", namedSessionTable);
 sqlDyStringPrintf(dy, "(userName, sessionName, contents, shared, "
                "firstUse, lastUse, useCount");
 if (gotSettings)
     sqlDyStringPrintf(dy, ", settings");
 sqlDyStringPrintf(dy, ") VALUES (");
 sqlDyStringPrintf(dy, "'%s', '%s', ", encUserName, encSessionName);
 sqlDyStringPrintf(dy, "'");
 cleanHgSessionFromCart(cart);
 struct dyString *encoded = dyStringNew(4096);
 cartEncodeState(cart, encoded);
 
 // First output the hubStatus id's for attached trackHubs
 outAttachedHubUrls(cart, encoded);
 
 // Now add all the default visibilities to output.
 outDefaultTracks(cart, encoded);
 
 sqlDyAppendEscaped(dy, encoded->string);
 dyStringFree(&encoded);
 sqlDyStringPrintf(dy, "', ");
 sqlDyStringPrintf(dy, "%d, ", sharingLevel);
 if (firstUse)
     sqlDyStringPrintf(dy, "'%s', ", firstUse);
 else
     sqlDyStringPrintf(dy, "now(), ");
 sqlDyStringPrintf(dy, "now(), %d", useCount);
 if (gotSettings)
     sqlDyStringPrintf(dy, ", '%s'", settings);
 sqlDyStringPrintf(dy, ")");
 sqlUpdate(conn, dy->string);
 dyStringFree(&dy);
 
 /* Prevent modification of custom track collections or quickLifts just saved to namedSessionDb: */
 cartCopyLocalHubs(cart);
 return useCount;
 }
 
 char *doNewSession(char *userName)
 /* Save current settings in a new named session.
  * Return a message confirming what we did. */
 {
 if (userName == NULL)
     return "Unable to save session -- please log in and try again.";
 struct dyString *dyMessage = dyStringNew(2048);
 char *sessionName = trimSpaces(cartString(cart, hgsNewSessionName));
 if (isEmpty(sessionName))
     return "Error: Unable to save a session without a name.  Please add one and try again.";
 
 char *encSessionName = cgiEncodeFull(sessionName);
 boolean shareSession = cartBoolean(cart, hgsNewSessionShare);
 char *encUserName = cgiEncodeFull(userName);
 struct sqlConnection *conn = hConnectCentral();
 
 if (sqlTableExists(conn, namedSessionTable))
     {
     int useCount = saveCartAsSession(conn, encUserName, encSessionName, shareSession);
     if (useCount > INITIAL_USE_COUNT)
 	dyStringPrintf(dyMessage,
 	  "Overwrote the contents of session <B>%s</B> "
 	  "(that %s be shared with other users).  "
 	  "%s %s",
 	  htmlEncode(sessionName), (shareSession ? "may" : "may not"),
 	  getSessionLink(encUserName, encSessionName),
 	  getSessionEmailLink(encUserName, encSessionName));
     else
 	dyStringPrintf(dyMessage,
 	  "Added a new session <B>%s</B> that %s be shared with other users.  "
 	  "%s %s",
 	  htmlEncode(sessionName), (shareSession ? "may" : "may not"),
 	  getSessionLink(encUserName, encSessionName),
 	  getSessionEmailLink(encUserName, encSessionName));
     if (shareSession)
         {
         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": <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(). */
 {
 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/<token> */
     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/<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);
         }
     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");
 if (convertPath == NULL)
     convertPath = cloneString("convert");
 char *whichCmd[] = {"which", convertPath, NULL};
 struct pipeline *pl = pipelineOpen1(whichCmd, pipelineWrite | pipelineNoAbort, "/dev/null", NULL, 0);
 int convertTestResult = pipelineWait(pl);
 
 if (convertTestResult != 0)
     {
     dyStringPrintf(dyMessage,
          "Note: A thumbnail image for this session was not created because the ImageMagick convert "
          "tool could not be found.  Please contact your mirror administrator to resolve this "
          "issue, either by installing convert so that it is part of the webserver's PATH, "
          "by adding the \"sessionThumbnail.convertPath\" option to the mirror's hg.conf file "
          "to specify the path to that program, or by adding \"sessionThumbnail.suppress=on\" to "
          "the mirror's hg.conf file to suppress this warning.<br>");
     return 0;
     }
 
 sqlSafef(query, sizeof(query),
     "select firstUse from %s where userName = \"%s\" and sessionName = \"%s\"",
     namedSessionTable, encUserName, encSessionName);
 char *firstUse = sqlNeedQuickString(conn, query);
 sqlSafef(query, sizeof(query), "select idx from gbMembers where userName = '%s'", encUserName);
 char *userIdx = sqlQuickString(conn, query);
 char *userIdentifier = sessionThumbnailGetUserIdentifier(encUserName, userIdx);
 char *destFile = sessionThumbnailFilePath(userIdentifier, encSessionName, firstUse);
 if (destFile != NULL)
     {
     struct dyString *hgTracksUrl = dyStringNew(0);
     addSessionLink(hgTracksUrl, encUserName, encSessionName, FALSE, FALSE);
     struct dyString *renderUrl =
         dyStringSub(hgTracksUrl->string, "cgi-bin/hgTracks", "cgi-bin/hgRenderTracks");
     dyStringAppend(renderUrl, "&pix=640");
     char *renderCmd[] = {"wget", "-q", "-O", "-", renderUrl->string, NULL};
     char *convertCmd[] = {convertPath, "-", "-resize", "320", "-crop", "320x240+0+0", destFile, NULL};
     char **cmdsImg[] = {renderCmd, convertCmd, NULL};
     pipelineOpen(cmdsImg, pipelineWrite, "/dev/null", NULL, 0);
     }
 return 1;
 }
 
 
 void thumbnailRemove(char *encUserName, char *encSessionName, struct sqlConnection *conn)
 /* Unlink thumbnail image for the gallery.  Leaks memory from a generated filename string. */
 {
 char query[4096];
 sqlSafef(query, sizeof(query),
     "select firstUse from %s where userName = \"%s\" and sessionName = \"%s\"",
     namedSessionTable, encUserName, encSessionName);
 char *firstUse = sqlNeedQuickString(conn, query);
 sqlSafef(query, sizeof(query), "select idx from gbMembers where userName = '%s'", encUserName);
 char *userIdx = sqlQuickString(conn, query);
 char *userIdentifier = sessionThumbnailGetUserIdentifier(encUserName, userIdx);
 char *filePath = sessionThumbnailFilePath(userIdentifier, encSessionName, firstUse);
 if (filePath != NULL)
     unlink(filePath);
 }
 
 static struct slName *getUserSessionNames(char *encUserName)
 /* Return a list of unencoded session names belonging to user. */
 {
 struct slName *existingSessionNames = NULL;
 struct sqlConnection *conn = hConnectCentral();
 char query[1024];
 sqlSafef(query, sizeof(query), "select sessionName from %s where userName = '%s';",
         namedSessionTable, encUserName);
 struct sqlResult *sr = sqlGetResult(conn, query);
 char **row;
 while ((row = sqlNextRow(sr)) != NULL)
     {
     char *encSessionName = row[0];
     char *sessionName = cgiDecodeClone(encSessionName);
     slNameAddHead(&existingSessionNames, sessionName);
     }
 sqlFreeResult(&sr);
 hDisconnectCentral(&conn);
 return existingSessionNames;
 }
 
 char *doSessionDetail(char *userName, char *sessionName)
 /* Show details about a particular session. */
 {
 if (userName == NULL)
     return "Sorry, please log in again.";
 struct dyString *dyMessage = dyStringNew(4096);
 char *encSessionName = cgiEncodeFull(sessionName);
 char *encUserName = cgiEncodeFull(userName);
 struct sqlConnection *conn = hConnectCentral();
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char query[512];
 webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
 if (gotSettings)
     sqlSafef(query, sizeof(query), "SELECT shared, firstUse, settings from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 else
     sqlSafef(query, sizeof(query), "SELECT shared, firstUse from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 sr = sqlGetResult(conn, query);
 if ((row = sqlNextRow(sr)) != NULL)
     {
     int shared = atoi(row[0]);
     char *firstUse = row[1];
     char *settings = NULL;
     if (gotSettings)
 	settings = row[2];
     char *description = getSetting(settings, "description");
     if (description == NULL) description = "";
 
     dyStringPrintf(dyMessage, "<A HREF=\"../goldenPath/help/hgSessionHelp.html#Details\" "
 		   "TARGET=_BLANK>Session Details Help</A><P/>\n");
 
 #define highlightAccChanges " var b = document.getElementById('" hgsDoSessionChange "'); " \
                             "  if (b) { b.style.background = '#ff9999'; }"
 
 #define toggleGalleryDisable \
                             "  var c = document.getElementById('detailsSharedCheckbox'); " \
                             "  var d = document.getElementById('detailsGalleryCheckbox'); " \
                             "  if (c.checked)" \
                             "    {d.disabled = false;} " \
                             "  else" \
                             "    {d.disabled = true; " \
                             "     d.checked = false; }"
 
     dyStringPrintf(dyMessage, "<B>%s</B><P>\n"
 		   "<FORM ACTION=\"%s\" NAME=\"detailForm\" METHOD=GET>\n"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=%s>"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=\"%s\">"
 		   "Session Name: "
 		   "<INPUT TYPE=TEXT NAME=\"%s\" id='%s' SIZE=%d VALUE=\"%s\" >\n",
 		   sessionName, hgSessionName(),
 		   cartSessionVarName(cart), cartSessionId(cart), hgsOldSessionName, sessionName,
 		   hgsNewSessionName, hgsNewSessionName, 32, sessionName);
     jsOnEventById("change"  , hgsNewSessionName, highlightAccChanges);
     jsOnEventById("keydown", hgsNewSessionName, highlightAccChanges);
 
     dyStringPrintf(dyMessage,
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"Accept changes\">"
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"Cancel\"> "
 		   "<BR>\n",
 		   hgsDoSessionChange, hgsDoSessionChange, 
 		   hgsCancel);
     struct slName *existingSessionNames = getUserSessionNames(encUserName);
     struct dyString *checkExistingNameJs = dyPrintCheckExistingSessionJs( existingSessionNames, sessionName);
     struct dyString *onClickJs = dyStringCreate(
                     "var pattern = /^\\s*$/;"
                     "if (document.getElementById(\"detailsGalleryCheckbox\").checked &&"
                     "   pattern.test(document.getElementById(\"%s\").value)) {"
                     "       warn('Please add a description to allow this session to be included in the Public Gallery');"
                     "       event.preventDefault();"
                     "} else {"
                     "       %s"
                     "}", hgsNewSessionDescription, checkExistingNameJs->string);
     jsOnEventById("click", hgsDoSessionChange, onClickJs->string);
     dyStringFree(&onClickJs);
     dyStringFree(&checkExistingNameJs);
 
     dyStringPrintf(dyMessage,
 		   "Share with others? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
 		   "id=\"detailsSharedCheckbox\">\n"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
 		   hgsSharePrefix, encSessionName, (shared>0 ? " CHECKED" : ""),
 		   cgiBooleanShadowPrefix(), hgsSharePrefix, encSessionName);
     jsOnEventByIdF("change", "detailsSharedCheckbox", "{%s %s}", highlightAccChanges, toggleGalleryDisable);
     jsOnEventByIdF("click" , "detailsSharedCheckbox", "{%s %s}", highlightAccChanges, toggleGalleryDisable);
 
     dyStringPrintf(dyMessage,
 		   "List in Public Sessions? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
 		   "id=\"detailsGalleryCheckbox\">\n"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
 		   hgsGalleryPrefix, encSessionName, (shared>=2 ? " CHECKED" : ""),
 		   cgiBooleanShadowPrefix(), hgsGalleryPrefix, encSessionName);
     jsOnEventById("change", "detailsGalleryCheckbox", highlightAccChanges);
     jsOnEventById("click" , "detailsGalleryCheckbox", highlightAccChanges);
     
     /* Set initial disabled state of the gallery checkbox */
     jsInline(toggleGalleryDisable);
     dyStringPrintf(dyMessage,
 		   "Created on %s.<BR>\n", firstUse);
     /* Print custom track counts per assembly */
     struct cart *tmpCart = cartNew(NULL,NULL,NULL,NULL);
     struct sqlConnection *conn2 = hConnectCentral();
     cartLoadUserSession(conn2, userName, sessionName, tmpCart, NULL, NULL);
     hDisconnectCentral(&conn2);
     hubConnectLoadHubs(tmpCart);
     cartCheckForCustomTracks(tmpCart, dyMessage);
 
     if (gotSettings)
         {
         description = replaceChars(description, "\\\\", "\\__ESC__");
         description = replaceChars(description, "\\r", "\r");
         description = replaceChars(description, "\\n", "\n");
         description = replaceChars(description, "\\__ESC__", "\\");
         char *encDescription = htmlEncode(description);
         dyStringPrintf(dyMessage,
             "Description:<BR>\n"
             "<TEXTAREA NAME=\"%s\" id='%s' ROWS=%d COLS=%d "
             ">%s</TEXTAREA><BR>\n",
             hgsNewSessionDescription, hgsNewSessionDescription, 5, 80,
             encDescription);
 	    jsOnEventById("change"   , hgsNewSessionDescription, highlightAccChanges);
 	    jsOnEventById("keypress" , hgsNewSessionDescription, highlightAccChanges);
         }
     dyStringAppend(dyMessage, "</FORM>\n");
     sqlFreeResult(&sr);
     }
 else
     errAbort("doSessionDetail: got no results from query:<BR>\n%s\n", query);
 
 return dyStringCannibalize(&dyMessage);
 }
 
 char *doUpdateSessions(char *userName)
 /* Look for cart variables matching prefixes for sharing/unsharing,
  * loading or deleting a previously saved session.
  * Return a message confirming what we did, or NULL if no such variables
  * were in the cart. */
 {
 if (userName == NULL)
     return NULL;
 struct dyString *dyMessage = dyStringNew(1024);
 struct hashEl *cartHelList = NULL, *hel = NULL;
 struct sqlConnection *conn = hConnectCentral();
 char *encUserName = cgiEncodeFull(userName);
 boolean didSomething = FALSE;
 char query[512];
 
 cartHelList = cartFindPrefix(cart, hgsGalleryPrefix);
 if (cartHelList != NULL)
     {
     struct hash *galleryHash = hashNew(0);
     char **row;
     struct sqlResult *sr;
     sqlSafef(query, sizeof(query),
 	  "select sessionName,shared from %s where userName = '%s'",
 	  namedSessionTable, encUserName);
     sr = sqlGetResult(conn, query);
     while ((row = sqlNextRow(sr)) != NULL)
 	    hashAddInt(galleryHash, row[0], atoi(row[1]));
     sqlFreeResult(&sr);
     for (hel = cartHelList;  hel != NULL;  hel = hel->next)
 	{
 	char *encSessionName = hel->name + strlen(hgsGalleryPrefix);
 	char *sessionName = cgiDecodeClone(encSessionName);
 	boolean inGallery = hashIntVal(galleryHash, encSessionName) >= 2 ? TRUE : FALSE;
 	boolean newGallery  = cartUsualInt(cart, hel->name, 0) > 0 ? TRUE : FALSE;
 
 	if (newGallery != inGallery)
 	    {
 	    sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d "
 		  "WHERE userName = '%s' AND sessionName = '%s';",
 		  namedSessionTable, newGallery == TRUE ? 2 : 1, encUserName, encSessionName);
 	    sqlUpdate(conn, query);
 	    sessionTouchLastUse(conn, encUserName, encSessionName);
 	    dyStringPrintf(dyMessage,
 			   "Marked session <B>%s</B> as %s.<BR>\n",
 			   htmlEncode(sessionName),
 			   (newGallery == TRUE ? "added to gallery" : "removed from public listing"));
             if (newGallery == FALSE)
                 thumbnailRemove(encUserName, encSessionName, conn);
             if (newGallery == TRUE)
                 thumbnailAdd(encUserName, encSessionName, conn, dyMessage);
 	    didSomething = TRUE;
 	    }
 	}
     hashFree(&galleryHash);
     }
 
 cartHelList = cartFindPrefix(cart, hgsSharePrefix);
 if (cartHelList != NULL)
     {
     struct hash *sharedHash = hashNew(0);
     char **row;
     struct sqlResult *sr;
     sqlSafef(query, sizeof(query),
 	  "select sessionName,shared from %s where userName = '%s'",
 	  namedSessionTable, encUserName);
     sr = sqlGetResult(conn, query);
     while ((row = sqlNextRow(sr)) != NULL)
 	hashAddInt(sharedHash, row[0], atoi(row[1]));
     sqlFreeResult(&sr);
     for (hel = cartHelList;  hel != NULL;  hel = hel->next)
 	{
 	char *encSessionName = hel->name + strlen(hgsSharePrefix);
 	char *sessionName = cgiDecodeClone(encSessionName);
 	boolean alreadyShared = hashIntVal(sharedHash, encSessionName) > 0 ? TRUE : FALSE;
     boolean inGallery = hashIntVal(sharedHash, encSessionName) >= 2 ? TRUE : FALSE;
 	boolean newShared = cartUsualInt(cart, hel->name, 1) ? TRUE : FALSE;
 
 	if (newShared != alreadyShared)
 	    {
 	    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);
 	    dyStringPrintf(dyMessage,
 			   "Marked session <B>%s</B> as %s.<BR>\n",
 			   htmlEncode(sessionName),
 			   (newShared == TRUE ? "shared" : "unshared"));
             if (newShared == FALSE && inGallery == TRUE)
                 thumbnailRemove(encUserName, encSessionName, conn);
 	    didSomething = TRUE;
 	    }
 	}
     hashFree(&sharedHash);
     }
 
 hel = cartFindPrefix(cart, hgsEditPrefix);
 if (hel != NULL)
     {
     char *encSessionName = hel->name + strlen(hgsEditPrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     dyStringPrintf(dyMessage, "%s", doSessionDetail(userName, sessionName));
     didSomething = TRUE;
     }
 
 hel = cartFindPrefix(cart, hgsLoadPrefix);
 if (hel != NULL)
     {
     char *encSessionName = hel->name + strlen(hgsLoadPrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     char wildStr[256];
     safef(wildStr, sizeof(wildStr), "%s*", hgsLoadPrefix);
     dyStringPrintf(dyMessage,
 		   "Loaded settings from session <B>%s</B>. %s %s<BR>\n",
 		   htmlEncode(sessionName),
 		   getSessionLink(encUserName, encSessionName),
 		   getSessionEmailLink(encUserName, encSessionName));
     cartLoadUserSession(conn, userName, sessionName, cart, NULL, wildStr);
     cartCopyLocalHubs(cart);
     hubConnectLoadHubs(cart);
     cartHideDefaultTracks(cart);
     cartCheckForCustomTracks(cart, dyMessage);
     didSomething = TRUE;
     }
 
 cartHelList = cartFindPrefix(cart, hgsDeletePrefix);
 for (hel = cartHelList;  hel != NULL;  hel = hel->next)
     {
     char *encSessionName = hel->name + strlen(hgsDeletePrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     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);
     dyStringPrintf(dyMessage,
 		   "Deleted session <B>%s</B>.<BR>\n",
 		   htmlEncode(sessionName));
     didSomething = TRUE;
     }
 
 hDisconnectCentral(&conn);
 if (didSomething)
     return(dyStringCannibalize(&dyMessage));
 else
     {
     dyStringFree(&dyMessage);
     return NULL;
     }
 }
 
 char *doOtherUser(char *actionVar)
 /* Load settings from another user's named session.
  * Return a message confirming what we did. */
 {
 struct sqlConnection *conn = hConnectCentral();
 struct dyString *dyMessage = dyStringNew(1024);
 char *otherUser = trimSpaces(cartString(cart, hgsOtherUserName));
 char *sessionName = trimSpaces(cartString(cart, hgsOtherUserSessionName));
 char *encOtherUser = cgiEncodeFull(otherUser);
 char *encSessionName = cgiEncodeFull(sessionName);
 
 dyStringPrintf(dyMessage,
        "Loaded settings from user <B>%s</B>'s session <B>%s</B>. %s %s",
 	       htmlEncode(otherUser), htmlEncode(sessionName),
 	       getSessionLink(encOtherUser, encSessionName),
 	       getSessionEmailLink(encOtherUser, encSessionName));
 cartLoadUserSession(conn, otherUser, sessionName, cart, NULL, actionVar);
 cartCopyLocalHubs(cart);
 hubConnectLoadHubs(cart);
 cartHideDefaultTracks(cart);
 cartCheckForCustomTracks(cart, dyMessage);
 hDisconnectCentral(&conn);
 return dyStringCannibalize(&dyMessage);
 }
 
 void doSaveLocal()
 /* Output current settings to be saved as a file on the user's machine.
  * Return a message confirming what we did. */
 {
 char *fileName = textOutSanitizeHttpFileName(cartString(cart, hgsSaveLocalFileName));
 char *compressType = cartString(cart, hgsSaveLocalFileCompress);
 struct pipeline *compressPipe = textOutInit(fileName, compressType, NULL);
 
 cleanHgSessionFromCart(cart);
 
 cartDumpHgSession(cart);
 
 // First output the hubStatus id's for attached trackHubs
 outAttachedHubUrls(cart, NULL);
 
 // Now add all the default visibilities to output.
 outDefaultTracks(cart, NULL);
 
 textOutClose(&compressPipe, NULL);
 }
 
 char *doLoad(boolean fromUrl, char *actionVar)
 /* Load settings from a file or URL sent by the user.
  * Return a message confirming what we did. */
 {
 struct dyString *dyMessage = dyStringNew(1024);
 struct lineFile *lf = NULL;
 webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
 if (fromUrl)
     {
     char *url = trimSpaces(cartString(cart, hgsLoadUrlName));
     if (isEmpty(url))
 	errAbort("Please go back and enter the URL (http://..., ftp://...) "
 		 "of a file that contains "
 		 "previously saved browser settings, and then click "
 		 "\"submit\" again.");
     if (!startsWith("http://",url) && !startsWith("https://",url) && !startsWith("ftp://",url))
         errAbort("Unsupported protocol for loading a file via URL.  Please use http, https, or ftp");
     lf = netLineFileOpen(url);
     dyStringPrintf(dyMessage, "Loaded settings from URL %s .  %s %s",
 		   htmlEncode(url), getUrlLink(url), getUrlEmailLink(url));
     }
 else
     {
     char *filePlainContents = cartOptionalString(cart, hgsLoadLocalFileName);
     char *fileBinaryCoords = cartOptionalString(cart,
 					hgsLoadLocalFileName "__binary");
     char *fileName = cartOptionalString(cart,
 					hgsLoadLocalFileName "__filename");
     if (isNotEmpty(filePlainContents))
 	{
 	char *settings = trimSpaces(filePlainContents);
 	dyStringAppend(dyMessage, "Loaded settings from local file ");
 	if (isNotEmpty(fileName))
 	    dyStringPrintf(dyMessage, "<B>%s</B> ", fileName);
 	dyStringPrintf(dyMessage, "(%lu bytes).",
 		       (unsigned long)strlen(settings));
 	lf = lineFileOnString("settingsFromFile", TRUE, cloneString(settings));
 	}
     else if (isNotEmpty(fileBinaryCoords))
 	{
 	/* The cart holds the address and size of the uploaded bytes, but any
 	 * request can set that variable, so only use a block cheapcgi
 	 * handed out. */
 	unsigned long size = 0;
 	char *mem = cgiMemBlobFind(fileBinaryCoords, &size);
 	lf = (mem == NULL) ? NULL : lineFileDecompressMem(TRUE, mem, size);
 	if (lf != NULL)
 	    {
 	    dyStringAppend(dyMessage, "Loaded settings from local file ");
 	    if (isNotEmpty(fileName))
 		dyStringPrintf(dyMessage, "<B>%s</B> ", fileName);
 	    dyStringPrintf(dyMessage, "(%lu bytes).", size);
 	    }
 	else
 	    dyStringPrintf(dyMessage,
 			   "Sorry, I don't recognize the file type of "
 			   "<B>%s</B>.  Please submit plain text or "
 			   "compressed text in one of the formats offered in "
 			   "<B>Save Settings</B>.", fileName);
 	}
     else
 	{
 	dyStringAppend(dyMessage, "Sorry, your web browser seems to have "
 		       "posted no data");
 	if (isNotEmpty(fileName))
 	    dyStringPrintf(dyMessage, ", only the filename <B>%s</B>",
 			   fileName);
 	dyStringAppend(dyMessage, " (empty file?).  Your settings have not been changed.");
 	lf = NULL;
 	}
     dyStringPrintf(dyMessage, "&nbsp;&nbsp;"
 	   "<A HREF=\"%shgTracks?%s=%s\">Browser</A>",
 	   hLocalHostCgiBinUrl(), 
 	   cartSessionVarName(), cartSessionId(cart));
     }
 if (lf != NULL)
     {
     lineFileCarefulNewlines(lf);
     struct dyString *dyLoadMessage = dyStringNew(0);
     boolean ok = cartLoadSettingsFromUserInput(lf, cart, NULL, actionVar, dyLoadMessage);
     lineFileClose(&lf);
     if (ok)
         {
         dyStringAppend(dyMessage, dyLoadMessage->string);
         cartCopyLocalHubs(cart);
         hubConnectLoadHubs(cart);
         cartHideDefaultTracks(cart);
         cartCheckForCustomTracks(cart, dyMessage);
         }
     else
         {
         dyStringClear(dyMessage);
         dyStringAppend(dyMessage, "<span style='color: red;'><b>"
                        "Unable to load session: </b></span>");
         dyStringAppend(dyMessage, dyLoadMessage->string);
         dyStringAppend(dyMessage, "The uploaded file needs to have been previously saved from the "
                        "<b>Save Settings</b> section.\n");
         // Looking for the words "custom track" in an error string is hokey, returning an enum
         // from cartLoadSettings would be better, but IMO that isn't worth a big refactoring.
         if (stringIn("custom track", dyLoadMessage->string))
             {
             dyStringPrintf(dyMessage, "If you would like to upload a custom track, please use the "
                            "<a href='%s?%s'>"
                            "Custom Tracks</a> tool.\n",
                            hgCustomName(), cartSidUrlString(cart));
             }
         dyStringAppend(dyMessage, "If you feel you have reached this "
                        "message in error, please contact the "
                        "<A HREF=\"mailto:genome-www@soe.ucsc.edu?subject=Session file upload failed&"
                        "body=Hello Genome Browser team,%0AMy session file failed to upload. "
                        "The error message was:%0A");
         dyStringAppend(dyMessage, cgiEncodeFull(dyLoadMessage->string));
         dyStringAppend(dyMessage, "%0ACan you help me upload the data?\">"
                        "UCSC Genome Browser team</A> for assistance.\n");
         }
     dyStringFree(&dyLoadMessage);
     }
 return dyStringCannibalize(&dyMessage);
 }
 
 void renamePrefixedCartVar(char *prefix, char *oldName, char *newName)
 /* If cart has prefix+oldName, replace it with prefix+newName = submit. */
 {
 char varName[256];
 safef(varName, sizeof(varName), "%s%s", prefix, oldName);
 if (cartVarExists(cart, varName))
     {
     cartRemove(cart, varName);
     safef(varName, sizeof(varName), "%s%s", prefix, newName);
     cartSetString(cart, varName, "submit");
     }
 }
 
 char *doSessionChange(char *userName, char *oldSessionName)
 /* Process changes to session from session details page. */
 {
 if (userName == NULL)
     return "Unable to make changes to session.  Please log in again.";
 struct dyString *dyMessage = dyStringNew(1024);
 webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
 char *sessionName = oldSessionName;
 char *encSessionName = cgiEncodeFull(sessionName);
 char *encOldSessionName = encSessionName;
 char *encUserName = cgiEncodeFull(userName);
 struct sqlConnection *conn = hConnectCentral();
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char query[512];
 int shared = 1;
 char *settings = NULL;
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
 if (gotSettings)
     sqlSafef(query, sizeof(query), "SELECT shared, settings from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 else
     sqlSafef(query, sizeof(query), "SELECT shared from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 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);
     // 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),
             "UPDATE %s set shared = %d WHERE userName = '%s' AND sessionName = '%s';",
             namedSessionTable, newShared, encUserName, encSessionName);
         sqlUpdate(conn, query);
         dyStringPrintf(dyMessage, "Marked session <B>%s</B> as %s.<BR>\n",
             htmlEncode(sessionName), (newShared>0 ? newShared>=2 ? "shared in public listing" :
                 "shared, but not in public listing" : "unshared"));
         if (shared >= 2 && newShared < 2)
             thumbnailRemove(encUserName, encSessionName, conn);
         if (shared < 2 && newShared >= 2)
             thumbnailAdd(encUserName, encSessionName, conn, dyMessage);
         }
     cartRemove(cart, sharedVarName);
     cartRemove(cart, galleryVarName);
     char shadowVarName[512];
     safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), sharedVarName);
     cartRemove(cart, shadowVarName);
     safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), galleryVarName);
     cartRemove(cart, shadowVarName);
     }
 if (gotSettings)
     {
     struct hash *settingsHash = raFromString(settings);
     char *description = hashFindVal(settingsHash, "description");
     char *newDescription = cartOptionalString(cart, hgsNewSessionDescription);
     if (newDescription != NULL)
 	{
 	// newline escaping of \n is needed for ra syntax.
         // not sure why \r and \ are being escaped, but it may be too late to change
         // since there are probably records in the database that way now.
 	newDescription = replaceChars(newDescription, "\\", "\\\\");
 	newDescription = replaceChars(newDescription, "\r", "\\r");
 	newDescription = replaceChars(newDescription, "\n", "\\n");
 	}
     else
 	newDescription = "";
     if (description == NULL)
 	description = "";
     if (!sameString(description, newDescription))
 	{
 	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);
 	dyStringPrintf(dyMessage, "Updated description of <B>%s</B>.\n", sessionName);
 	}
     }
 if (isEmpty(dyMessage->string))
     dyStringPrintf(dyMessage, "No changes to session <B>%s</B>.\n", sessionName);
 dyStringPrintf(dyMessage, "%s %s",
 	       getSessionLink(encUserName, encSessionName),
 	       getSessionEmailLink(encUserName, encSessionName));
 if (shared)
     printShareMessage(dyMessage, encUserName, encSessionName, FALSE);
 return dyStringCannibalize(&dyMessage);
 }
 
 static int getSharingLevel(struct sqlConnection *conn, char *encUserName, char *encSessionName)
 /* Return the value of 'shared' from the namedSessionDb row for user & session;
  * errAbort if there is no such session. (0 = not shared, 1 = shared by link, 2 = public session) */
 {
 char query[2048];
 sqlSafef(query, sizeof(query), "select shared from %s where userName='%s' and sessionName = '%s';",
          namedSessionTable, encUserName, encSessionName);
 char buf[256];
 char *sharedStr = sqlQuickQuery(conn, query, buf, sizeof buf);
 if (sharedStr == NULL)
     errAbort("Unable to find session for userName='%s' and sessionName='%s'; no result from query '%s'",
              encUserName, encSessionName, query);
 return atoi(sharedStr);
 }
 
 char *doReSaveSession(char *userName, char *actionVar)
 /* Load a session (which may have old trash and customTrash references) and re-save it
  * so that customTrash tables will be moved to customData* databases and trash paths
  * will be replaced with userdata (hg.conf sessionDataDir) paths.
  * NOTE: this is not intended to be reachable by the UI; it is for a script to update
  * old sessions to use the new sessionData locations. */
 {
 if (userName == NULL)
     return "Unable to re-save session -- please log in and try again.";
 struct sqlConnection *conn = hConnectCentral();
 char *sessionName = trimSpaces(cartString(cart, hgsNewSessionName));
 if (isEmpty(sessionName))
     return "Error: Unable to save a session without a name.  Please add one and try again.";
 
 char *encUserName = cgiEncodeFull(userName);
 char *encSessionName = cgiEncodeFull(sessionName);
 int sharingLevel = getSharingLevel(conn, encUserName, encSessionName);
 cartLoadUserSession(conn, userName, sessionName, cart, NULL, actionVar);
 // Don't cartCopyLocalHubs because we're not going to make any track collection changes
 hubConnectLoadHubs(cart);
 // Some old sessions reference databases that are no longer present, and that triggers an errAbort
 // when cartHideDefaultTracks calls hgTrackDb.  Don't let that stop the process of updating other
 // stuff in the session.
 struct errCatch *errCatch = errCatchNew();
 if (errCatchStart(errCatch))
     cartHideDefaultTracks(cart);
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     fprintf(stderr, "doReSaveSession: Error from cartHideDefaultTracks: '%s'; Continuing...",
             errCatch->message->string);
 errCatchFree(&errCatch);
 struct dyString *dyMessage = dyStringNew(1024);
 dyStringPrintf(dyMessage,
                "Re-saved settings from user <B>%s</B>'s session <B>%s</B> "
                "that %s be shared with others.  %s %s",
 	       htmlEncode(userName), htmlEncode(sessionName), (sharingLevel ? "may" : "may not"),
 	       getSessionLink(encUserName, encSessionName),
 	       getSessionEmailLink(encUserName, encSessionName));
 cartCheckForCustomTracks(cart, dyMessage);
 int useCount = saveCartAsSession(conn, encUserName, encSessionName, sharingLevel);
 if (useCount <= INITIAL_USE_COUNT)
     errAbort("Expected useCount of at least %d after re-saving session for "
              "userName='%s', sessionName='%s', but got %d",
              INITIAL_USE_COUNT+1, encUserName, encSessionName, useCount);
 hDisconnectCentral(&conn);
 return dyStringCannibalize(&dyMessage);
 }
 
 // ======================================
 
 void prepBackGroundCall(char **pBackgroundProgress, char *cleanPrefix)
 /* fix cart and save state */
 {
 *pBackgroundProgress = cloneString(cgiUsualString("backgroundProgress", NULL)); 
 cartRemove(cart, "backgroundExec");
 cartRemove(cart, "backgroundProgress");
 cartRemovePrefix(cart, cleanPrefix);
 cartSaveState(cart);  // in case it crashes
 }
 
 void launchForeAndBackGround(char *operation)
 /* update cart, launch background and foreground */
 {
 char cmd[1024]; 
 safef(cmd, sizeof cmd, "./hgSession backgroundExec=%s", operation);
 
 // allow child to see variables loaded from CGI.
 // because CGI settings have not been saved back to the cart yet
 cartSaveState(cart);   
 
 char *workUrl = NULL;
 // automatically adds hgsid
 // automatically adds backGroundProgress=%s url.progress for separate channel
 // Have to pass the userName manually since background exec will not get cookie,
 // but we are no longer using userName which was needed with saved-sessions.
 startBackgroundWork(cmd, &workUrl);
 
 htmlOpen("Background Status");
 
 jsInlineF(
     "setTimeout(function(){location = 'hgSession?backgroundStatus=%s&hgsid=%s';},2000);\n", 
     cgiEncode(workUrl), cartSessionId(cart));
 htmlClose();
 fflush(stdout);
 }
 
 void passSubmittedBinaryAsTrashFile(struct hashEl *list)
 /* fetch the binary file submitted in memory,
  * and save it to temp trash location,
  * saving the name in the cart. 
  * This is necessary to pass the file to the background process.*/
 {
 // List should have these two
 // hgS_extractUpload_hub_9614_Anc11__binary
 // hgS_extractUpload_hub_9614_Anc11__filename
 // can have a third __filepath if crashes leaving in the cart.
 
 char *binaryParam = NULL;
 struct hashEl *hel = NULL;
 for (hel = list; hel; hel = hel->next)
     {
     if (endsWith(hel->name, "__binary"))
 	binaryParam = cloneString(hel->name);
     }
 
 if (!binaryParam)
     {
     htmlOpen("No file selected");
     printf("Please choose a saved session custom tracks local backup archive file (.tar.gz) to upload");
     htmlClose();
     exit(0);
     } 
 
 char *binaryValue = cartOptionalString(cart, binaryParam);
 
 /* The cart holds the address and size of the uploaded bytes, but any request
  * can set that variable, so only use a block cheapcgi handed out. */
 unsigned long size = 0;
 char *mem = cgiMemBlobFind(binaryValue, &size);
 if (mem == NULL)
     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("<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",
            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_<id>_<acc>" 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. */
 {
+cspWriteResponseHeader();
 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("<div class='gbBanner'>%s</div>\n", message);
 
 printf("<div id='sessionApp' class='gbApp'></div>\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[, <extraFields>]} 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));
 
 if (backgroundStatus)
     {
     // clear backgroundStatus from the cart
     cartRemove(cart, "backgroundStatus");
     /* Save cart variables. */
     cartSaveState(cart);
     getBackgroundStatus(backgroundStatus);
     exit(0);
     }
 
 char *backgroundExec = cloneString(cgiUsualString("backgroundExec", NULL));
 
 
 struct hashEl *showDownloadList = cartFindPrefix(cart, hgsShowDownloadPrefix);
 struct hashEl *makeDownloadList = cartFindPrefix(cart, hgsMakeDownloadPrefix);
 struct hashEl *doDownloadList = cartFindPrefix(cart, hgsDoDownloadPrefix);
 
 if (showDownloadList)
     showDownloadSessionCtData(showDownloadList);
 else if (makeDownloadList)
     {
     if (sameOk(backgroundExec,"makeDownloadSessionCtData"))
 	{
 	// only one, not a list.
 	struct hashEl *hel = makeDownloadList;
 	char *param1 = cloneString(hel->name);
 
 	char *backgroundProgress = NULL; 
 	prepBackGroundCall(&backgroundProgress, hgsMakeDownloadPrefix);
 
 	makeDownloadSessionCtData(param1, backgroundProgress);
 
 	exit(0);  // cannot return
 	}
     else
 	{
 
 	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, 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))
     {
     char *message = doLoad(TRUE, hgsDoLoadUrl);
     doMainPage(userName, message);
     }
 else if (cartVarExists(cart, hgsDoSessionDetail))
     {
     char *message = doSessionDetail(userName, cartString(cart, hgsDoSessionDetail));
     doMainPage(userName, message);
     }
 else if (cartVarExists(cart, hgsDoSessionChange))
     {
     char *message = doSessionChange(userName, cartString(cart, hgsOldSessionName));
     doMainPage(userName, message);
     }
 else if (cartVarExists(cart, hgsOldSessionName))
     {
     char *message1 = doSessionChange(userName, cartString(cart, hgsOldSessionName));
     char *message2 = doUpdateSessions(userName);
     char *message = message2;
     if (!startsWith("No changes to session", message1))
 	{
 	size_t len = (sizeof message1[0]) * (strlen(message1) + strlen(message2) + 1);
 	message = needMem(len);
 	safef(message, len, "%s%s", message1, message2);
 	}
     doMainPage(userName, message);
     }
 else if (cartVarExists(cart, hgsDoReSaveSession))
     {
     char *message = doReSaveSession(userName, hgsDoReSaveSession);
     printf("\n%s\n\n", message);
     }
 else
     {
     char *message = doUpdateSessions(userName);
     doMainPage(userName, message);
     }
 
 cleanHgSessionFromCart(cart);
 /* Save the cart state: */
 cartCheckout(&cart);
 }
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 long enteredMainTime = clock1000();
 htmlPushEarlyHandlers();
 cgiSpoof(&argc, argv);
 hgSession();
 cgiExitTime("hgSession", enteredMainTime);
 return 0;
 }