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>&nbsp;&nbsp;&nbsp;</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>&nbsp;&nbsp;&nbsp;</TD><TD>user: \n");
 cgiMakeOnKeypressTextVar(hgsOtherUserName,
 			 cartUsualString(cart, hgsOtherUserName, ""),
 			 20, "return noSubmitOnEnter(event);");
 printf("&nbsp;&nbsp;&nbsp;session name: \n");
 cgiMakeOnKeypressTextVar(hgsOtherUserSessionName,
 			 cartUsualString(cart, hgsOtherUserSessionName, ""),
@@ -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("&nbsp;&nbsp;");
 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>&nbsp;&nbsp;&nbsp;</TD><TD>name:</TD><TD>\n");
     cgiMakeOnKeypressTextVar(hgsNewSessionName,
 			     cartUsualString(cart, "db", NULL),
 			     20, jsPressOnEnter(hgsDoNewSession));
     printf("&nbsp;&nbsp;&nbsp;");
     cgiMakeCheckBox(hgsNewSessionShare,
 		    cartUsualBoolean(cart, hgsNewSessionShare, TRUE));
     printf("allow this session to be loaded by others\n");
     printf("</TD><TD>");
     printf("&nbsp;");
     if (existingSessionNames)
 	{
-	struct dyString *js = 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>&nbsp;&nbsp;&nbsp;</TD><TD>file:</TD><TD>\n");
 cgiMakeOnKeypressTextVar(hgsSaveLocalFileName,
 			 cartUsualString(cart, hgsSaveLocalFileName, ""),
 			 20, jsPressOnEnter(hgsDoSaveLocal));
 printf("&nbsp;&nbsp;&nbsp;");
@@ -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,
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"use\">"
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" id='%s%s' VALUE=\"delete\">"
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"accept changes\">"
 		   "&nbsp;&nbsp;<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)