cb99f0b11bdeee5dfa76064d38b6410da0f4a709
max
Thu Sep 10 00:55:21 2026 -0700
Centralize CGI Content-Type printing in one cgiPrintContentType() helper
Around 90 places across the tree hand-rolled the CGI response header, each
with its own spelling: "Content-Type:" or "Content-type:", \n or \r\n, and
the terminating blank line written as part of the same string, as a separate
puts("\n") (which emits two newlines, so a stray blank line led the body) or
as printf("\r\n\r\n") (two blank lines). A handful forgot the terminator
entirely and relied on a following header to supply it.
cgiPrintContentType() in lib/cheapcgi.c now writes the Content-Type line and
the blank line that ends the header. Header lines are not ordered, so the
callers that also send Status, Set-Cookie, Content-Disposition, Content-Length
or X-Sendfile write those first and call this last to close the header; that
keeps it to a single helper rather than a print-the-line / end-the-header pair
that a caller can half-use. cart.c's existing httpHeaders list already worked
this way.
Only the CGI response path is touched. The dyStringPrintf("Content-type: ...")
calls that build outgoing HTTP *requests* (genomeSpace, oauthLogin, eapMetaSync,
edwWebAuthLogin, ga4ghToBed) are unrelated and left alone.
Also fills out the apiKey error message in botDelay.c to say where to create a
key and that keys are server-specific.
No behavior change on the wire beyond dropping those stray blank lines and
adding the missing newline after Retry-After.
diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index 1ac413e611e..8c062c68dc5 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -1,2942 +1,2942 @@
/* 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 "snapshotSession.h"
#include "jsonParse.h"
#include "jsonWrite.h"
#include "perfTimer.h"
char *database = NULL;
struct perfTimer *hgSessionTiming = NULL; /* Non-NULL when &measureTiming is set; times the page
* and is emitted as hgSessionData.timing for the JS. */
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);
/* Gallery thumbnail helpers, defined further below with the rest of the gallery code. The AJAX
* endpoints above them have to keep a thumbnail in step with its session, so they need these. */
int thumbnailAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn,
struct dyString *dyMessage);
void thumbnailRemove(char *encUserName, char *encSessionName, struct sqlConnection *conn);
char *cgiDecodeClone(char *encStr)
/* Allocate and return a CGI-decoded copy of encStr. */
{
size_t len = strlen(encStr);
char *decStr = needMem(len+1);
cgiDecode(encStr, decStr, len);
return decStr;
}
void welcomeUser(char *wikiUserName)
/* Tell the user they are not logged in to the wiki or other login
* system and tell them how to do so. */
{
char *wikiHost = wikiLinkHost();
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);
char *changeRecovEmailUrl = wikiLinkChangeRecovEmailUrl(cartSessionId(cart));
if (changeRecovEmailUrl != NULL)
printf("<li><A HREF=\"%s\">Recovery email</A></li>", changeRecovEmailUrl);
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, " <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> ",
dyTmp->string);
printCopyToClipboardButton(dy, "copyIcon", "urlText", " 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 <BR>details </th>"
"<th style=\"text-align:left\">Delete this <BR>session </th>"
"<th style=\"text-align:left\">Share with <BR>others? </th>"
"<th style=\"text-align:left\">Post in <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];
/* A snapshot is a share token, not a session the user made and would recognize (see
* lib/snapshotSession.c). Leave it out of the list, as its "__" prefix promises. */
if (snapshotIsSnapshotName(encSessionName))
continue;
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> </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(" ");
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(" </TD>"
"<TD data-order=\"%ld\"><nobr>%s</nobr> </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> </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> </TD><TD>User: \n");
cgiMakeOnKeypressTextVar(hgsOtherUserName,
cartUsualString(cart, hgsOtherUserName, ""),
20, "return noSubmitOnEnter(event);");
printf(" Session name: \n");
cgiMakeOnKeypressTextVar(hgsOtherUserSessionName,
cartUsualString(cart, hgsOtherUserSessionName, ""),
20, jsPressOnEnter(hgsDoOtherUser));
printf(" ");
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(" ");
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(" ");
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> </TD><TD>Name:</TD><TD>\n");
cgiMakeOnKeypressTextVar(hgsNewSessionName,
hubConnectSkipHubPrefix(cartUsualString(cart, "db", "mySession")),
20, jsPressOnEnter(hgsDoNewSession));
printf(" ");
cgiMakeCheckBox(hgsNewSessionShare,
cartUsualBoolean(cart, hgsNewSessionShare, TRUE));
printf("Allow this session to be loaded by others\n");
printf("</TD><TD>");
printf(" ");
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> </TD><TD>File:</TD><TD>\n");
cgiMakeOnKeypressTextVar(hgsSaveLocalFileName,
cartUsualString(cart, hgsSaveLocalFileName, ""),
20, jsPressOnEnter(hgsDoSaveLocal));
printf(" ");
printf("File type returned: ");
char *compressType = cartUsualString(cart, hgsSaveLocalFileCompress, textOutCompressNone);
cgiMakeRadioButton(hgsSaveLocalFileCompress, textOutCompressNone,
differentWord(textOutCompressGzip, compressType));
printf(" plain text ");
cgiMakeRadioButton(hgsSaveLocalFileCompress, textOutCompressGzip,
sameWord(textOutCompressGzip, compressType));
printf(" gzip compressed (ignored if output file is blank)");
printf("</TD><TD>");
printf(" ");
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> </TD><TD colspan=2>");
printf("Back up custom tracks to archive .tar.gz</TD>");
printf("<TD>");
printf(" ");
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");
+cgiPrintContentType("text/html");
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);
/* Two of the Save form's own inputs. If they stay in the cart they are stored inside every
* session saved afterwards, so one session's description travels in sessions that never had
* one. hgsNewSessionShare is left alone on purpose: it has the same problem, but it is also
* the only memory of the user's "Allow this session to be loaded by others" choice, and
* showSavingOptions() defaults that box to checked. Removing it here would quietly re-check
* the box for someone who keeps their sessions private. */
cartRemove(cart, hgsNewSessionName);
cartRemove(cart, hgsNewSessionDescription);
}
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 the custom track collection just saved to namedSessionDb. Under
* copy-on-write hgCollection asks for its own trash copy before it writes, so this does
* nothing. refs #38273 */
cartCopyLocalHubsOnSessionLoad(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);
/* Clone: saveCartAsSession() removes this cart variable, which frees the cart's own copy. */
char *sessionName = trimSpaces(cloneString(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");
+cgiPrintContentType("application/json");
printf("{\"error\": \"%s\"}\n", jsonStringEscape(message));
hDisconnectCentral(&conn);
}
static boolean namedSessionExists(struct sqlConnection *conn, char *encUserName,
char *encSessionName)
/* Is there already a session by this name for this user? Both names must be encoded the way they
* are stored, i.e. through cgiEncodeFull(). */
{
char query[1024];
sqlSafef(query, sizeof query,
"select count(*) from %s where userName = '%s' and sessionName = '%s'",
namedSessionTable, encUserName, encSessionName);
return sqlQuickNum(conn, query) > 0;
}
static void saveSessionJsonResult(struct sqlConnection *conn, char *encUserName,
char *encSessionName, char *sessionName, char *warning)
/* Emit {"name": ..., "url": ...} for the "Share a link" AJAX endpoints and disconnect.
* sessionName is the human-readable (decoded) name; the client uses it as the rename "old name".
* warning (may be NULL) is added as "warning" for something that went wrong alongside a save that
* did succeed, such as a thumbnail the server could not build. */
{
struct dyString *dyUrl = dyStringNew(0);
addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE);
-puts("Content-Type:application/json\n");
+cgiPrintContentType("application/json");
printf("{\"name\": \"%s\", \"url\": \"%s\"", jsonStringEscape(sessionName),
jsonStringEscape(dyUrl->string));
if (isNotEmpty(warning))
printf(", \"warning\": \"%s\"", jsonStringEscape(warning));
puts("}");
dyStringFree(&dyUrl);
hDisconnectCentral(&conn);
}
static int sessionSharedLevel(struct sqlConnection *conn, char *encUserName, char *encSessionName)
/* Return the sharing level of this user's session: 0 private, 1 shared by link, 2 in the public
* listing. Returns -1 when the user has no session by that name, which the shared column cannot
* express (it is NOT NULL), so the AJAX endpoints can say so instead of reporting a no-op as a
* success. */
{
char query[512];
sqlSafef(query, sizeof(query), "select shared from %s where userName = '%s' and sessionName = '%s'",
namedSessionTable, encUserName, encSessionName);
char *shared = sqlQuickString(conn, query);
if (shared == NULL)
return -1;
return atoi(shared);
}
static char *thumbnailWarning(struct dyString *dyMessage)
/* Return what thumbnailAdd had to say for itself, as plain text for a JSON reply, or NULL when it
* said nothing. The message is written for HTML output, so take the <br> back out. */
{
if (dyMessage == NULL || dyMessage->stringSize == 0)
return NULL;
return trimSpaces(replaceChars(dyMessage->string, "<br>", " "));
}
void doAnonNameJson()
/* AJAX endpoint that reserves a fresh, guaranteed-unique anonymous snapshot name and returns it as
* JSON {"name": ...} WITHOUT saving anything. The top-right "Share a link" dialog calls this on open
* so it can show the exact link as a preview before the user commits, while keeping name generation
* server-side (unique, crypto-strong) for every anonymous link. */
{
struct sqlConnection *conn = hConnectCentral();
cartRemove(cart, hgsDoAnonName);
if (!sqlTableExists(conn, namedSessionTable))
{
saveSessionJsonError(conn, "Required session table does not exist in the central database.");
return;
}
char *name = snapshotNewName(conn, "l");
-puts("Content-Type:application/json\n");
+cgiPrintContentType("application/json");
printf("{\"name\": \"%s\"}\n", jsonStringEscape(name));
hDisconnectCentral(&conn);
}
void doSaveSessionJson(char *userName)
/* AJAX endpoint behind the "Share a link" menu button. Save the current cart as a named session
* and print JSON {"name": <session name>, "url": <shareable link>}. When the user is not logged
* in (or hgsShareAnon is set), save under the reserved anonymous user "l" with a random token
* name. When logged in, the caller supplies the name (a typed name, or a random internal
* "_XXXXXXXX" name generated client-side). Saved shared by link so the link works for anyone.
* Reuses saveCartAsSession() and addSessionLink(). */
{
struct sqlConnection *conn = hConnectCentral();
if (!sqlTableExists(conn, namedSessionTable))
{
saveSessionJsonError(conn, "Required session table does not exist in the central database.");
return;
}
boolean anon = isEmpty(userName) || cgiBoolean(hgsShareAnon);
boolean failIfExists = cgiBoolean(hgsFailIfExists);
// A registered snapshot type (e.g. "blat") means: save a lightweight snapshot holding only that
// feature's declared cart vars, not the whole cart (see lib/snapshotSession.c).
char *snapshotType = cgiOptionalString(hgsSnapshotType);
// Read the requested name from the request, not the cart. cleanHgSessionFromCart() now takes
// this variable back out, but carts written before that still hold a sticky value from
// hgSession's Save form, and it would otherwise shadow ours.
char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsNewSessionName, "")));
/* Keep our control variables out of the saved session contents and the user's own cart. */
cartRemove(cart, hgsDoSaveSessionJson);
cartRemove(cart, hgsShareAnon);
cartRemove(cart, hgsFailIfExists);
cartRemove(cart, hgsSnapshotType);
cartRemove(cart, hgsNewSessionName);
cartRemove(cart, hgsNewSessionShare);
/* Snapshot path: a lightweight session holding only the feature's declared cart vars, under a
* server-generated, guaranteed-unique "__"-prefixed name (share tokens must never collide and
* silently overwrite one another). Handled before the normal full-session logic because its
* naming rules differ. Works for both anonymous ("l") and logged-in owners. */
if (isNotEmpty(snapshotType))
{
struct snapshotType *st = snapshotTypeFind(snapshotType);
if (st == NULL)
{
saveSessionJsonError(conn, "Unknown snapshot type.");
return;
}
/* Refuse to mint a link that would reopen to nothing (e.g. BLAT results not built yet); tell the
* caller to retry rather than handing out a dead link. */
if (!snapshotHasRequired(st, cart))
{
saveSessionJsonError(conn, "These results are not ready yet. Please try again in a moment.");
return;
}
char *snapUser = anon ? "l" : cgiEncodeFull(userName);
char *snapName;
if (isEmpty(sessionName))
snapName = snapshotNewName(conn, snapUser); /* server-generated, unique */
else
{
if (startsWith(snapshotNamePrefix, sessionName))
snapName = cgiEncodeFull(sessionName);
else
snapName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName));
/* Anonymous names are not the caller's to reuse; see the anon branch below. */
if (anon && namedSessionExists(conn, snapUser, snapName))
{
saveSessionJsonError(conn, "That link already exists.");
return;
}
}
saveSnapshotSession(conn, snapshotType, snapUser, snapName, cart);
char *snapDecoded = cgiDecodeClone(snapName);
saveSessionJsonResult(conn, snapUser, snapName, snapDecoded, NULL);
return;
}
char *encUserName = NULL;
char *encSessionName = NULL;
if (anon)
{
encUserName = "l"; /* reserved anonymous user -> short link /s/l/<token> */
/* Every anonymous share uses the shared snapshot naming: a server-generated, guaranteed-unique
* "__"-token, so tokens never collide/overwrite and the snapshot cleaner can remove abandoned
* ones. The top-right Share dialog passes a name it just reserved (for its live preview); we
* force the "__" prefix either way so the link stays eligible for cleaning.
* A name that came with the request is only ever one the dialog just reserved, which does not
* exist yet. Anonymous links all sit under the single reserved user "l", so a name already in
* the table stays as it is and the caller is told so, rather than being written over. */
if (isEmpty(sessionName))
encSessionName = snapshotNewName(conn, encUserName);
else
{
if (startsWith(snapshotNamePrefix, sessionName))
encSessionName = cgiEncodeFull(sessionName);
else
encSessionName = catTwoStrings(snapshotNamePrefix, cgiEncodeFull(sessionName));
if (namedSessionExists(conn, encUserName, encSessionName))
{
saveSessionJsonError(conn, "That link already exists.");
return;
}
}
sessionName = cgiDecodeClone(encSessionName); // keep decoded name in sync for the JSON result
}
else
{
/* Logged-in callers always supply a name: the caller either typed one or generated a random
* internal "_XXXXXXXX" name client-side (sessRandomShareName in hgSession.js, shared by the
* top-right "Share a link" menu in topLinks.js), so we no longer auto-name here. */
if (isEmpty(sessionName))
{
saveSessionJsonError(conn, "Please provide a name for this session.");
return;
}
encUserName = cgiEncodeFull(userName);
encSessionName = cgiEncodeFull(sessionName);
/* The Share dialog sets failIfExists when the user typed a custom name, so it can warn before
* clobbering an existing session of theirs. Report the clash instead of overwriting. */
if (failIfExists && namedSessionExists(conn, encUserName, encSessionName))
{
- puts("Content-Type:application/json\n");
+ cgiPrintContentType("application/json");
printf("{\"exists\": true}\n");
hDisconnectCentral(&conn);
return;
}
}
saveCartAsSession(conn, encUserName, encSessionName, 1); /* shared by link */
saveSessionJsonResult(conn, encUserName, encSessionName, sessionName, NULL);
}
void doRenameSessionJson(char *userName)
/* AJAX endpoint for the "Specify name" step of the Share dialog: rename an existing session
* (hgsOldSessionName -> hgsNewSessionName) under the current user. Logged-in only. Rejects a
* name already in use rather than overwriting it. Returns {"name","url"} or {"error"}. */
{
struct sqlConnection *conn = hConnectCentral();
// Read the names from the request, not the cart: hgSession's Save form also uses these variables
// and leaves a sticky value (e.g. the username) in the cart that would otherwise shadow ours.
char *oldName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, "")));
char *newName = trimSpaces(cloneString(cgiUsualString(hgsNewSessionName, "")));
cartRemove(cart, hgsDoRenameSessionJson);
cartRemove(cart, hgsOldSessionName);
cartRemove(cart, hgsNewSessionName);
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, NULL);
return;
}
/* Reject a name the user is already using (don't clobber an existing saved session). */
sqlSafef(query, sizeof query, "select count(*) from %s where userName = '%s' and sessionName = '%s'",
namedSessionTable, encUserName, encNewName);
if (sqlQuickNum(conn, query) > 0)
{
saveSessionJsonError(conn, "You already have a session with that name. Please pick another.");
return;
}
int shared = sessionSharedLevel(conn, encUserName, encOldName);
if (shared < 0)
{
saveSessionJsonError(conn, "Could not find the link to rename.");
return;
}
/* A gallery thumbnail's file name is built from the encoded session name, so the picture has to be
* moved along with the session or the public listing is left pointing at nothing. Take the old one
* away first, while the row still answers to the old name (the file name also carries firstUse,
* which is read from that row). */
if (shared >= 2)
thumbnailRemove(encUserName, encOldName, conn);
/* Same UPDATE that doSessionChange uses to rename a session. */
sqlSafef(query, sizeof query,
"UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';",
namedSessionTable, encNewName, encUserName, encOldName);
sqlUpdate(conn, query);
char *warning = NULL;
if (shared >= 2)
{
struct dyString *dyMessage = dyStringNew(256);
thumbnailAdd(encUserName, encNewName, conn, dyMessage);
warning = thumbnailWarning(dyMessage);
}
saveSessionJsonResult(conn, encUserName, encNewName, newName, warning);
}
int thumbnailAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn, struct dyString *dyMessage)
/* Create a thumbnail image for the gallery. If the necessary tools can't be found,
* add a warning message to dyMessage unless the hg.conf setting
* sessionThumbnail.suppressWarning is set to "on".
* Leaks memory from a generated filename string, plus a couple of dyStrings.
* Returns without determining if image creation succeeded (it happens in a separate
* thread); the return value is 0 if a message was added to dyMessage, otherwise it's 1. */
{
char query[4096];
char *suppressConvert = cfgOption("sessionThumbnail.suppress");
if (suppressConvert != NULL && sameString(suppressConvert, "on"))
return 1;
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,
" <INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"Accept changes\">"
" <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);
cartCopyLocalHubsOnSessionLoad(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);
cartCopyLocalHubsOnSessionLoad(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");
/* The name arrives with the upload and is printed in four of the messages below,
* so encode it once here rather than at each one. */
if (isNotEmpty(fileName))
fileName = htmlEncode(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, " "
"<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);
cartCopyLocalHubsOnSessionLoad(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);
// A thumbnail's file name is built from the encoded session name, so take the old picture away
// before the rename, while the row still answers to the old name.
if (shared >= 2)
thumbnailRemove(encUserName, encSessionName, conn);
// In case the user has clicked to confirm that they want to overwrite an existing session,
// delete the existing row before updating the row that will overwrite it.
sqlSafef(query, sizeof(query), "delete from %s where userName = '%s' and sessionName = '%s';",
namedSessionTable, encUserName, encNewName);
sqlUpdate(conn, query);
sqlSafef(query, sizeof(query),
"UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';",
namedSessionTable, encNewName, encUserName, encSessionName);
sqlUpdate(conn, query);
dyStringPrintf(dyMessage, "Changed session name from %s to <B>%s</B>.\n",
sessionName, newName);
sessionName = newName;
encSessionName = encNewName;
renamePrefixedCartVar(hgsEditPrefix , encOldSessionName, encNewName);
renamePrefixedCartVar(hgsLoadPrefix , encOldSessionName, encNewName);
renamePrefixedCartVar(hgsDeletePrefix , encOldSessionName, encNewName);
renamePrefixedCartVar(hgsShowDownloadPrefix , encOldSessionName, encNewName);
renamePrefixedCartVar(hgsMakeDownloadPrefix , encOldSessionName, encNewName);
renamePrefixedCartVar(hgsDoDownloadPrefix , encOldSessionName, encNewName);
if (shared >= 2)
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();
/* Clone: cartLoadUserSession() and saveCartAsSession() both free the cart's own copy. */
char *sessionName = trimSpaces(cloneString(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);
// No cartCopyLocalHubsOnSessionLoad 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. Advertising the new page depends on sessionNewPageBanner (defaulting to sessionNewPage),
* like hgBlat's printNewFormBanner, but the way back off the new page is always printed: the
* sessionNewPage cart variable sticks, so someone who reached the page by typing the variable into
* the URL on a machine where the banner is off would otherwise be stuck there. On the new page
* gbModern.css supplies .gbBanner; the classic page does not load it, so emit a small inline style
* there. */
{
if (!onNewPage && !cfgOptionBooleanDefault("sessionNewPageBanner",
cfgOptionBooleanDefault("sessionNewPage", FALSE)))
return;
if (onNewPage)
printf("<div class='gbBanner'>You are using the new experimental Sessions page. "
"<a href='hgSession?sessionNewPage=0&%s=%s'>Return to the classic page</a>. "
"If you have feedback, please let us know at "
"<a href='mailto:genome@soe.ucsc.edu'>genome@soe.ucsc.edu</a>.</div>\n",
cartSessionVarName(), cartSessionId(cart));
else
{
printf("<style>.gbBannerClassic{background:#fbf3e2;border:1px solid #d9bd82;padding:10px 14px;"
"margin:12px 0;font-size:14px;}</style>\n");
printf("<div class='gbBannerClassic'>We are testing a new Sessions page, with a searchable, "
"sortable table and one-click sharing. "
"<a href='hgSession?sessionNewPage=1&%s=%s'>Try the new page</a>.</div>\n",
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
perfTimerStep(hgSessionTiming, "page header + 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);
perfTimerStep(hgSessionTiming, "load sessions from MySQL");
char **row;
/* Cache one connection per assembly db so the per-session band/locus lookups don't
* re-open a connection for every row when many sessions share an assembly. */
struct hash *dbConnCache = hashNew(0);
while ((row = sqlNextRow(sr)) != NULL)
{
char *encSessionName = row[0];
/* Snapshots are share tokens, not sessions the user made; keep them out of the list,
* as their "__" prefix promises (see lib/snapshotSession.c). */
if (snapshotIsSnapshotName(encSessionName))
continue;
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);
/* created = date only for display; createdFull = date+minute for the hover. */
char *dateOnly = cloneString(firstUse);
char *spacePt = strchr(dateOnly, ' ');
if (spacePt != NULL)
*spacePt = '\0';
char *createdFull = cloneString(firstUse);
if (strlen(createdFull) == 19)
createdFull[16] = '\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);
char *lastUseDate = cloneString(lastUse); /* date only for display */
char *sp2 = strchr(lastUseDate, ' ');
if (sp2 != NULL)
*sp2 = '\0';
/* Trim the seconds off the full value shown on hover: "...10:29:25" -> "...10:29". */
if (strlen(lastUse) == 19)
lastUse[16] = '\0';
/* Band (from cytoBand) and locus (from locusName) for the saved position, looked up in
* the session's own assembly. Skips hub assemblies and missing tables. For a very
* large region there are too many genes to name, so say so instead. */
char *band = NULL, *locus = NULL;
if (isNotEmpty(db2) && isNotEmpty(pos2) && !trackHubDatabase(db2))
{
char *posClone = cloneString(pos2);
stripChar(posClone, ',');
char *colon = strrchr(posClone, ':');
char *chrom = NULL;
int start = 0, end = 0;
boolean parsed = FALSE;
if (colon != NULL)
{
*colon = '\0';
chrom = posClone;
char *dash = strchr(colon + 1, '-');
if (dash != NULL)
{
*dash = '\0';
start = atoi(colon + 1) - 1; /* display is 1-based; tables are 0-based */
if (start < 0)
start = 0;
end = atoi(dash + 1);
parsed = (end > start);
}
}
if (parsed)
{
struct sqlConnection *dbConn = hashFindVal(dbConnCache, db2);
if (dbConn == NULL && sqlDatabaseExists(db2))
{
dbConn = hAllocConn(db2);
hashAdd(dbConnCache, db2, dbConn);
}
if (dbConn != NULL)
{
if (sqlTableExists(dbConn, "cytoBand"))
{
char bandBuf[HDB_MAX_BAND_STRING];
if (hChromBandConn(dbConn, chrom, start, bandBuf) && bandBuf[0])
band = cloneString(bandBuf);
}
if ((end - start) > 10000000)
locus = cloneString("Too large for the genes");
else
locus = hLocusName(dbConn, chrom, start, end);
}
}
freez(&posClone);
}
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);
jsonWriteString(jw, "createdFull", createdFull);
jsonWriteNumber(jw, "createdEpoch", epoch);
jsonWriteString(jw, "lastUse", lastUse);
jsonWriteString(jw, "lastUseDate", lastUseDate);
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(band))
jsonWriteString(jw, "band", band);
if (isNotEmpty(locus))
jsonWriteString(jw, "locus", locus);
if (isNotEmpty(description))
jsonWriteString(jw, "description", description);
jsonWriteString(jw, "shareUrl", dyUrl->string);
jsonWriteObjectEnd(jw);
dyStringFree(&dyUrl);
freez(&band);
freez(&locus);
freez(&firstUse);
freez(&dateOnly);
freez(&createdFull);
freez(&lastUse);
freez(&lastUseDate);
freez(&sessionName);
}
sqlFreeResult(&sr);
perfTimerStep(hgSessionTiming, "annotate positions (band + locus) + build JSON");
/* Release the cached per-assembly connections. */
struct hashEl *hel, *helList = hashElListHash(dbConnCache);
for (hel = helList; hel != NULL; hel = hel->next)
{
struct sqlConnection *dbConn = hel->val;
hFreeConn(&dbConn);
}
hashElFreeList(&helList);
hashFree(&dbConnCache);
}
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. */
{
if (isNotEmpty(cartOptionalString(cart, "measureTiming")))
hgSessionTiming = perfTimerNew(); /* times the page; emitted as hgSessionData.timing */
cspWriteResponseHeader();
-puts("Content-Type:text/html\n");
+cgiPrintContentType("text/html");
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);
/* When &measureTiming is set, hand the per-phase timings to hgSession.js (it shows them in a
* dialog). Emitted at the top level as hgSessionData.timing. */
perfTimerJson(hgSessionTiming, jw, "timing");
jsonWriteObjectEnd(jw);
jsInlineF("var hgSessionData = %s;\n", jw->dy->string);
jsonWriteFree(&jw);
perfTimerFree(&hgSessionTiming);
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");
+cgiPrintContentType("application/json");
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];
int shared = sessionSharedLevel(conn, encUserName, encSessionName);
if (shared < 0)
{ saveSessionJsonError(conn, "Could not find that session."); return; }
if (shared >= 2)
thumbnailRemove(encUserName, encSessionName, conn);
sqlSafef(query, sizeof(query), "DELETE FROM %s WHERE userName = '%s' AND sessionName = '%s';",
namedSessionTable, encUserName, encSessionName);
sqlUpdate(conn, query);
saveSessionJsonOk(conn, NULL);
}
void doShareSessionJson(char *userName)
/* AJAX: set the "shared by link" flag (0<->1) on hgsOldSessionName. Desired state in
* hgsNewSessionShare (0/1). Does not touch the gallery (shared==2) except to unshare. */
{
struct sqlConnection *conn = hConnectCentral();
char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, "")));
int desired = cgiUsualInt(hgsNewSessionShare, 0);
cartRemove(cart, hgsNewSessionShare);
if (isEmpty(userName))
{ saveSessionJsonError(conn, "Please log in and try again."); return; }
if (isEmpty(sessionName))
{ saveSessionJsonError(conn, "No session was specified."); return; }
char *encUserName = cgiEncodeFull(userName);
char *encSessionName = cgiEncodeFull(sessionName);
char query[512];
int shared = sessionSharedLevel(conn, encUserName, encSessionName);
if (shared < 0)
{ saveSessionJsonError(conn, "Could not find that session."); return; }
int newShared = desired ? 1 : 0;
sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d WHERE userName = '%s' AND sessionName = '%s';",
namedSessionTable, newShared, encUserName, encSessionName);
sqlUpdate(conn, query);
sessionTouchLastUse(conn, encUserName, encSessionName);
/* Either way out of the public listing takes the picture with it: this endpoint drops a session
* from shared==2 to 1 as well as to 0, and the file would otherwise be left behind. */
if (shared >= 2 && newShared < 2)
thumbnailRemove(encUserName, encSessionName, conn);
char extra[32];
safef(extra, sizeof(extra), ", \"shared\": %d", newShared);
saveSessionJsonOk(conn, extra);
}
void doGallerySessionJson(char *userName)
/* AJAX: add/remove hgsOldSessionName to/from the public gallery (shared 2<->1). Desired state in
* hgsNewSessionShare (0/1). Adding requires a non-empty description, like the classic page. */
{
struct sqlConnection *conn = hConnectCentral();
char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, "")));
int desired = cgiUsualInt(hgsNewSessionShare, 0);
cartRemove(cart, hgsNewSessionShare);
if (isEmpty(userName))
{ 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;
}
}
int shared = sessionSharedLevel(conn, encUserName, encSessionName);
if (shared < 0)
{ saveSessionJsonError(conn, "Could not find that session."); return; }
int newShared = desired ? 2 : 1;
sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d WHERE userName = '%s' AND sessionName = '%s';",
namedSessionTable, newShared, encUserName, encSessionName);
sqlUpdate(conn, query);
sessionTouchLastUse(conn, encUserName, encSessionName);
struct dyString *dyMsg = dyStringNew(256);
if (desired && shared < 2)
thumbnailAdd(encUserName, encSessionName, conn, dyMsg);
if (!desired && shared >= 2)
thumbnailRemove(encUserName, encSessionName, conn);
/* Pass on anything thumbnailAdd had to say, e.g. that this mirror has no ImageMagick convert. The
* session is listed either way, but without this the reply is a bare success and the listing simply
* shows no picture. */
struct dyString *dyExtra = dyStringNew(64);
dyStringPrintf(dyExtra, ", \"shared\": %d", newShared);
char *warning = thumbnailWarning(dyMsg);
if (warning != NULL)
dyStringPrintf(dyExtra, ", \"warning\": \"%s\"", jsonStringEscape(warning));
saveSessionJsonOk(conn, dyExtra->string);
}
void doOverwriteSessionJson(char *userName)
/* AJAX: re-save the current cart over an existing session (hgsOldSessionName), preserving its
* sharing level. Returns the refreshed created date, view count and assembly. */
{
struct sqlConnection *conn = hConnectCentral();
char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsOldSessionName, "")));
if (isEmpty(userName))
{ saveSessionJsonError(conn, "Please log in and try again."); return; }
if (isEmpty(sessionName))
{ saveSessionJsonError(conn, "No session was specified."); return; }
if (!sqlTableExists(conn, namedSessionTable))
{ saveSessionJsonError(conn, "Required session table does not exist."); return; }
char *encUserName = cgiEncodeFull(userName);
char *encSessionName = cgiEncodeFull(sessionName);
char query[1024];
int shared = sessionSharedLevel(conn, encUserName, encSessionName);
if (shared < 0)
{ saveSessionJsonError(conn, "Could not find that session to overwrite."); return; }
int useCount = saveCartAsSession(conn, encUserName, encSessionName, shared);
/* Report the refreshed values so the table row can update in place. */
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];
if (sessionSharedLevel(conn, encUserName, encSessionName) < 0)
{ saveSessionJsonError(conn, "Could not find that session."); return; }
sqlSafef(query, sizeof(query), "select settings from %s where userName = '%s' and sessionName = '%s';",
namedSessionTable, encUserName, encSessionName);
char *settings = sqlQuickString(conn, query);
struct hash *settingsHash = raFromString(isEmpty(settings) ? "" : settings);
/* ra syntax needs \n / \r / backslash escaped (kept compatible with doSessionChange). */
newDescription = replaceChars(newDescription, "\\", "\\\\");
newDescription = replaceChars(newDescription, "\r", "\\r");
newDescription = replaceChars(newDescription, "\n", "\\n");
hashRemove(settingsHash, "description");
hashAdd(settingsHash, "description", newDescription);
struct dyString *dyRa = dyStringNew(512);
struct hashEl *hel = hashElListHash(settingsHash);
while (hel != NULL)
{
dyStringPrintf(dyRa, "%s %s\n", hel->name, (char *)hel->val);
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, hgsDoAnonName))
{
doAnonNameJson();
}
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;
}