a8787479d4851e364fa680e19d68fe5743b096be angie Thu Sep 12 13:10:19 2019 -0700 When changing session name, detect if the new name would overwrite an existing session and confirm with user. Then allow overwrite if confirmed. refs #24133 diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c index 24c7a55..69e6a37 100644 --- src/hg/hgSession/hgSession.c +++ src/hg/hgSession/hgSession.c @@ -41,31 +41,30 @@ #include "errCatch.h" 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}; -struct slName *existingSessionNames = 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') + '?');" 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) @@ -266,34 +265,35 @@ 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; } -void showExistingSessions(char *userName) +static struct slName *showExistingSessions(char *userName) /* Print out a table with buttons for sharing/unsharing/loading/deleting - * previously saved sessions. */ + * 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\":[1,'asc'],\n" @@ -403,30 +403,31 @@ 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("</div>\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, ""), @@ -457,69 +458,84 @@ 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"); } -void showSavingOptions(char *userName) +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, cartUsualString(cart, "db", NULL), 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 = 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.toUpperCase(); 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 + '. Proceed?'); } }"); + 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(" "); @@ -570,37 +586,38 @@ printf("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)) - showExistingSessions(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.\n", wikiLinkUserLoginUrl(cartSessionId(cart))); -showSavingOptions(userName); +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. " @@ -940,30 +957,51 @@ /* Unlink thumbnail image for the gallery. Leaks memory from a generated filename string. */ { char query[4096]; sqlSafef(query, sizeof(query), "select firstUse from namedSessionDb where userName = \"%s\" and sessionName = \"%s\"", 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); @@ -999,42 +1037,46 @@ " {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("keypress", hgsNewSessionName, highlightAccChanges); + jsOnEventById("keydown", hgsNewSessionName, highlightAccChanges); dyStringPrintf(dyMessage, " <INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"use\">" " <INPUT TYPE=SUBMIT NAME=\"%s%s\" id='%s%s' VALUE=\"delete\">" " <INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"accept changes\">" " <INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"cancel\"> " "<BR>\n", hgsLoadPrefix, encSessionName, hgsDeletePrefix, encSessionName, hgsDeletePrefix, encSessionName, hgsDoSessionChange, hgsDoSessionChange, hgsCancel); + struct slName *existingSessionNames = getUserSessionNames(encUserName); + struct dyString *js = dyPrintCheckExistingSessionJs( existingSessionNames, sessionName); + jsOnEventById("click", hgsDoSessionChange, js->string); + dyStringFree(&js); char id[256]; safef(id, sizeof id, "%s%s", hgsDeletePrefix, encSessionName); jsOnEventByIdF("click", id, confirmDeleteFormat, encSessionName); 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, @@ -1444,30 +1486,35 @@ sr = sqlGetResult(conn, query); if ((row = sqlNextRow(sr)) != NULL) { shared = atoi(row[0]); if (gotSettings) settings = cloneString(row[1]); sqlFreeResult(&sr); } else errAbort("doSessionChange: got no results from query:<BR>\n%s\n", query); char *newName = trimSpaces(cartOptionalString(cart, hgsNewSessionName)); if (isNotEmpty(newName) && !sameString(sessionName, newName)) { char *encNewName = cgiEncodeFull(newName); + // In case the user has clicked to confirm that they want to overwrite an existing session, + // delete the existing row before updating the row that will overwrite it. + sqlSafef(query, sizeof(query), "delete from %s where userName = '%s' and sessionName = '%s';", + namedSessionTable, encUserName, encNewName); + sqlUpdate(conn, query); sqlSafef(query, sizeof(query), "UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';", namedSessionTable, encNewName, encUserName, encSessionName); sqlUpdate(conn, query); dyStringPrintf(dyMessage, "Changed session name from %s to <B>%s</B>.\n", sessionName, newName); sessionName = newName; encSessionName = encNewName; renamePrefixedCartVar(hgsEditPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsLoadPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsDeletePrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsShowDownloadPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsMakeDownloadPrefix , encOldSessionName, encNewName); renamePrefixedCartVar(hgsDoDownloadPrefix , encOldSessionName, encNewName); if (shared >= 2)