f3009e2f9d911795bd1d13a0f95bbbee46309f83
galt
  Sun Nov 14 18:54:39 2021 -0800
Adding warn-and-continue option to hgCustom. Motivation is to support https warnings but will be useful more widely. refs #28458

diff --git src/hg/hgCustom/hgCustom.c src/hg/hgCustom/hgCustom.c
index 250b19a..5a93aee 100644
--- src/hg/hgCustom/hgCustom.c
+++ src/hg/hgCustom/hgCustom.c
@@ -72,31 +72,31 @@
 #define hgCtDoDelete	  hgCtDo "delete"
 #define hgCtDoDeleteSet	  hgCtDo "delete_set"
 #define hgCtDoDeleteClr	  hgCtDo "delete_clr"
 #define hgCtDoRefresh     hgCtDo "refresh"
 #define hgCtDoRefreshSet  hgCtDo "refresh_set"
 #define hgCtDoRefreshClr  hgCtDo "refresh_clr"
 #define hgCtDoGenomeBrowser	  hgCtDo "gb"
 #define hgCtDoTableBrowser	  hgCtDo "tb"
 #ifdef PROGRESS_METER
 #define hgCtDoProgress	  hgCtDo "progress"
 #endif
 
 /* Global variables */
 struct cart *cart;
 struct hash *oldVars = NULL;
-char *excludeVars[] = {"Submit", "submit", "SubmitFile", NULL};
+char *excludeVars[] = {"Submit", "submit", "SubmitFile", "ContinueWithWarn", NULL};
 char *database = NULL;
 char *organism = NULL;
 struct customTrack *ctList = NULL;
 
 void makeClearButton(char *field)
 /* UI button that clears a text field */
 {
 char id[256];
 safef(id, sizeof id, "%s_clear", field);
 char javascript[1024];
 safef(javascript, sizeof javascript,
         "document.mainForm.%s.value = '';", field);
 cgiMakeOnClickButton(id, javascript, " Clear ");
 }
 
@@ -136,31 +136,31 @@
 " <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html#TRACK'>track</A>\n"
 " and"
 " <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html#BROWSER'>browser</A>\n"
 " line attributes as described in the \n"
 " <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html'>User's Guide</A>.<br>\n"
 " Examples are\n"
 " <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html#EXAMPLE1'>here</A>.\n"
 " If you do not have web-accessible data storage available, please see the\n"
 " <A TARGET=_BLANK HREF='../goldenPath/help/hgTrackHubHelp.html#Hosting'>Hosting</A> section of the Track Hub Help documentation.\n<br><br>"
 " Please note a much more efficient way to load data is to use\n"
 " <A TARGET=_BLANK HREF='../goldenPath/help/hgTrackHubHelp.html'>Track Hubs</A>, which are loaded\n" 
 " from the <A HREF='hgHubConnect'>Track Hubs Portal</A> found in the menu under My Data.\n"
 );
 }
 
-void addCustomForm(struct customTrack *ct, char *err)
+void addCustomForm(struct customTrack *ct, char *err, boolean warnOnly)
 /* display UI for adding custom tracks by URL or pasting data */
 {
 char *dataUrl = NULL;
 char buf[1024];
 
 boolean gotClade = FALSE;
 boolean isUpdateForm = FALSE;
 if (ct)
     {
     isUpdateForm = TRUE;
     dataUrl = ctDataUrl(ct);
     }
 else
     /* add form needs clade for assembly menu */
     gotClade = hGotClade();
@@ -211,60 +211,66 @@
     puts("</TD></TR></TABLE>\n");
     }
 
 /* intro text */
 puts("<P>");
 if (isUpdateForm)
     puts("Update your custom track configuration, data, and/or documentation.");
 else
     puts("Display your own data as custom annotation tracks in the browser.");
 addIntro();
 puts("<P>");
 
 /* row for error message */
 if (isNotEmpty(err))
     {
-    printf("<P><B>&nbsp;&nbsp;&nbsp;&nbsp;<span style='color:RED; font-style:italic;'>"
-           "Error</span>&nbsp;%s</B><P>", err);
+    char *fullErrString = replaceChars(err, "\n", "<br>\n");
+    printf("<P><B>&nbsp;&nbsp;&nbsp;&nbsp;<span style='color:%s; font-style:italic;'>"
+           "%s</span><P>%s</B><P>", warnOnly ? "ORANGE" : "RED", warnOnly ? "Warning" : "Error", fullErrString);
+    freeMem(fullErrString);
     /* send two lines of the message to the apache error log also: */
-    char *tmpString = cloneString(err);
-    char *lineBreak = strchr(tmpString, '\n');
-    if (lineBreak)  /* first line break becomes a blank */
-        *lineBreak = ' ';
-    lineBreak = strchr(tmpString, '\n');
-    if (lineBreak)  /* second one becomes end of string */
-        *lineBreak = (char) 0;
+    char *tmpString = replaceChars(err, "\n", " ");
     fprintf(stderr, "hgCustom load error: %s\n", tmpString);
+    freeMem(tmpString);
     }
 
 cgiSimpleTableStart();
 
 /* first rows for update form are for track and browser line entry */
 if (isUpdateForm)
     {
     /* row for instructions */
     cgiSimpleTableRowStart();
     cgiSimpleTableFieldStart();
     if (dataUrl)
         puts("Configuration:");
     else
         {
         puts("Edit configuration:");
         }
     cgiTableFieldEnd();
     cgiTableField("&nbsp;");
     puts("<TD ALIGN='RIGHT'>");
+    if (warnOnly)
+	{
+	cgiMakeButtonWithOnClick("ContinueWithWarn", "Continue with Warning", NULL, "return submitClick(this);");
+	printf("&nbsp;");
+	jsInline(
+	    "$('textarea').change(function() {\n"
+	    "    $('#ContinueWithWarn').hide();\n"
+	    "});\n");
+	}
     cgiMakeButtonWithOnClick("Submit", "Submit", NULL, "return submitClick(this);");
     printf("<img id='loadingImg' src='../images/loading.gif' />\n");
     cgiTableFieldEnd();
     cgiTableField("&nbsp;");
     cgiTableRowEnd();
 
     /* row for text entry box */
     cgiSimpleTableRowStart();
     puts("<TD COLSPAN=2>");
     if (dataUrl)
         {
         /* can't update via pasting if loaded from URL */
         cgiMakeTextAreaDisableable(hgCtConfigLines,
             cartUsualString(cart, hgCtConfigLines, customTrackUserConfig(ct)),
                             CONFIG_ENTRY_ROWS, TEXT_ENTRY_COLS, TRUE);
@@ -307,30 +313,39 @@
 else
     cgiTableField("Paste URLs or data:");
 
 if (isUpdateForm && dataUrl)
     cgiTableField("&nbsp");
 else
     {
     puts("<TD ALIGN='RIGHT'>");
     puts("Or upload: ");
     cgiMakeFileEntry(hgCtDataFile);
     cgiTableFieldEnd();
     }
 if (!isUpdateForm)
     {
     cgiSimpleTableFieldStart();
+    if (warnOnly)
+	{
+	cgiMakeButtonWithOnClick("ContinueWithWarn", "Continue with Warning", NULL, "return submitClick(this);");
+	printf("&nbsp;");
+	jsInline(
+	    "$('textarea').change(function() {\n"
+	    "    $('#ContinueWithWarn').hide();\n"
+	    "});\n");
+	}
     cgiMakeButtonWithOnClick("Submit", "Submit", NULL, "return submitClick(this);");
     printf("<img id='loadingImg' src='../images/loading.gif' />\n");
     cgiTableFieldEnd();
     }
 cgiTableRowEnd();
 
 /* next row - text entry box for  data, and clear button */
 cgiSimpleTableRowStart();
 puts("<TD COLSPAN=2>");
 if (dataUrl)
     {
     /* can't update via pasting if loaded from URL */
     safef(buf, sizeof buf, "Replace data at URL: %s", ctDataUrl(ct));
     cgiMakeTextAreaDisableable(hgCtDataText, buf,
                                 TEXT_ENTRY_ROWS, TEXT_ENTRY_COLS, TRUE);
@@ -938,52 +953,52 @@
 #endif
 
 static void webBotWarning()
 /* display the overuse warning message in the javaScript warning text box
  * html output has already started at this point, just need to add the
  * warning handler and setup the warning javaScript box, warnings after
  * this will go to that text box
  */
 {
 pushWarnHandler(webVaWarn);
 htmlWarnBoxSetup(stdout);
 char *ip = getenv("REMOTE_ADDR");
 botDelayMessage(ip, botDelayMillis);
 }
 
-void doAddCustom(char *err)
+void doAddCustom(char *err, boolean warnOnly)
 /* display form for adding custom tracks.
  * Include error message, if any */
 {
 cartWebStart(cart, database, "Add Custom Tracks");
-addCustomForm(NULL, err);
+addCustomForm(NULL, err, warnOnly);
 helpCustom();
 if (issueBotWarning)
     webBotWarning();
 cartWebEnd(cart);
 }
 
-void doUpdateCustom(struct customTrack *ct, char *err)
+void doUpdateCustom(struct customTrack *ct, char *err, boolean warnOnly)
 /* display form for adding custom tracks.
  * Include error message, if any */
 {
 char *longLabel = htmlEncode(ct->tdb->longLabel);
 cartWebStart(cart, database, "Update Custom Track: %s [%s]",
         longLabel, database);
 freeMem(longLabel);
 cartSetString(cart, hgCtDocText, ct->tdb->html);
-addCustomForm(ct, err);
+addCustomForm(ct, err, warnOnly);
 helpCustom();
 cartWebEnd(cart);
 }
 
 static void doManageCustom(char *warnMsg)
 /* display form for deleting & updating custom tracks.
  * Include warning message, if any */
 {
 cartWebStart(cart, database, "Manage Custom Tracks");
 jsIncludeFile("jquery.js", NULL);
 manageCustomForm(warnMsg);
 webNewSection("Managing Custom Tracks");
 webIncludeHelpFile("customTrackManage", FALSE);
 if (issueBotWarning)
     webBotWarning();
@@ -1183,66 +1198,68 @@
             break;
         if (sameString(organism, dbDb->organism))
             {
             if (!dbWithCts)
                 dbWithCts = cloneString(dbDb->name);
             }
         }
     if (dbWithCts)  // set the database for the selected organism to an assembly that
         {           // has custom tracks
         database = dbWithCts;
         cartSetString(cart, "db", database);
         }
     }
 
 if (cartVarExists(cart, hgCtDoAdd))
-    doAddCustom(NULL);
+    doAddCustom(NULL, FALSE);
 #ifdef PROGRESS_METER
 else if (cartVarExists(cart, hgCtDoProgress))
     {
     doProgress(NULL);
     }
 #endif
 else if (cartVarExists(cart, hgCtTable))
     {
     /* update track */
     /* need to clone the hgCtTable value, as the ParseCart will remove
        the variable */
     selectedTable = cloneString(cartString(cart, hgCtTable));
     if (isNotEmpty(selectedTable))
         {
         ctList = customTracksParseCart(database, cart, NULL, NULL);
         ct = ctFromList(ctList, selectedTable);
         }
     if (ct)
-        doUpdateCustom(ct, NULL);
+        doUpdateCustom(ct, NULL, FALSE);
     else
-        doAddCustom(NULL);
+        doAddCustom(NULL, FALSE);
     }
 else
     {
     /* get new and existing custom tracks from cart and decide what to do */
 
     // setup a timer to periodically print out something to stdout to make sure apache or the web browser doesn't time us out (see redmine #3002).
     // e.g. see http://stackoverflow.com/questions/5547166/how-to-avoid-cgi-timeout
     struct sigaction *act;
     AllocVar(act);
     act->sa_handler = timer;
     act->sa_flags = SA_RESTART;
     sigaction(SIGALRM, act, NULL);
     alarm(TIMER_INTERVAL);
 
+    boolean warnOnly = FALSE;
+
     char *customText = fixNewData(cart);
     /* save input so we can display if there's an error */
     char *savedCustomText = saveLines(cloneString(customText),
                                 SAVED_LINE_COUNT);
     char *trackConfig = cartOptionalString(cart, hgCtConfigLines);
     char *savedConfig = cloneString(trackConfig);
 
     struct dyString *dsWarn = dyStringNew(0);
     char *fileName = cartOptionalString(cart, hgCtDataFileName);
     boolean hasData = (isNotEmpty(customText) || isNotEmpty(fileName));
     if (cartVarExists(cart, hgCtUpdatedTrack) && hasData)
         {
         /* from 'update' screen */
         /* prepend config to data for parser */
         struct dyString *dsTrack = dyStringNew(0);
@@ -1264,41 +1281,44 @@
             if (startsWith(trackConfig, customText))
                 trackConfig = "";
             else
                 customText = "Duplicate track configuration found - remove track and browser lines from Configuration box or from Data";
             }
         dyStringPrintf(dsTrack, "%s\n%s\n", trackConfig, customText);
         customText = dyStringCannibalize(&dsTrack);
         cartSetString(cart, hgCtDataText, customText);
         if (isNotEmpty(fileContents))
             {
             /* already handled file */
             cartRemove(cart, hgCtDataFile);
             cartRemove(cart, hgCtDataFileName);
             }
         }
+    warnOnly = FALSE;
     struct errCatch *catch = errCatchNew();
     if (errCatchStart(catch))
 	ctList = customTracksParseCartDetailed(database, cart, &browserLines, &ctFileName,
-					       &replacedCts, NULL, &err);
+					       &replacedCts, NULL, &err, &warnOnly);
     errCatchEnd(catch);
     if (catch->gotError)
 	{
 	addWarning(dsWarn, err);
 	}
     if (isNotEmpty(catch->message->string))
+	{
         addWarning(dsWarn, catch->message->string);
+	}
     errCatchFree(&catch);
 
     /* exclude special setting used by table browser to indicate
      * db assembly for error-handling purposes only */
     char *db = NULL;
     if (trackConfig && (db = stringIn("db=", trackConfig)) != NULL)
         {
         db += 3;
         char *nextTok = skipToSpaces(db);
         if (!nextTok)
             nextTok = strchr(db, 0);
         db = cloneStringZ(db,nextTok-db);
         if (!sameString(db,database))
             err = "Invalid configuration found - remove db= or return it to its original value. ";
         }
@@ -1317,84 +1337,86 @@
                     {
                     customTrackUpdateFromConfig(ct, database, trackConfig, &browserLines);
                     ctUpdated = TRUE;
                     }
                 errCatchEnd(catch);
                 if (isNotEmpty(catch->message->string))
                     err = catTwoStrings(emptyForNull(err), catch->message->string);
                 errCatchFree(&catch);
                 }
             }
         }
 
     addWarning(dsWarn, replacedTracksMsg(replacedCts));
     doBrowserLines(browserLines, &warnMsg);
     addWarning(dsWarn, warnMsg);
-    if (err)
+    if (err && !(warnOnly && cartVarExists(cart, "ContinueWithWarn")))
 	{
         char *selectedTable = NULL;
         cartSetString(cart, hgCtDataText, savedCustomText);
         cartSetString(cart, hgCtConfigLines, savedConfig);
         if ((selectedTable= cartOptionalString(cart, hgCtUpdatedTable)) != NULL)
             {
             ct = ctFromList(ctList, selectedTable);
-            doUpdateCustom(ct, err);
+            doUpdateCustom(ct, err, warnOnly);
             }
         else
-            doAddCustom(err);
+            doAddCustom(err, warnOnly);
        	cartRemovePrefix(cart, hgCt);
 	return;
 	}
     if (cartVarExists(cart, hgCtDoDelete))
         {
 	doDeleteCustom();
         ctUpdated = TRUE;
         }
     if (cartVarExists(cart, hgCtDoRefresh))
 	{
 	doRefreshCustom(&warnMsg);
 	addWarning(dsWarn, warnMsg);
         ctUpdated = TRUE;
 	}
     if (ctUpdated || ctConfigUpdate(ctFileName))
 	{
 	customTracksSaveCart(database, cart, ctList);
 
 	/* refresh ctList again to pickup remote resource error state */
+	warnOnly=FALSE;
 	struct errCatch *catch = errCatchNew();
 	if (errCatchStart(catch))
 	    ctList = customTracksParseCartDetailed(database, cart, &browserLines, &ctFileName,
-					       &replacedCts, NULL, &err);
+					       &replacedCts, NULL, &err, &warnOnly);
 	errCatchEnd(catch);
 	if (catch->gotError)
 	    {
 	    addWarning(dsWarn, err);
 	    addWarning(dsWarn, catch->message->string);
 	    }
 	errCatchFree(&catch);
-
 	}
     warnMsg = dyStringCannibalize(&dsWarn);
     if (measureTiming)
 	{
 	long lastTime = clock1000();
 	loadTime = lastTime - thisTime;
 	}
+
+
     if (ctList || cartVarExists(cart, hgCtDoDelete))
         doManageCustom(warnMsg);
     else
-	doAddCustom(warnMsg);
+	doAddCustom(warnMsg, warnOnly);
     }
 cartRemovePrefix(cart, hgCt);
 cartRemove(cart, CT_CUSTOM_TEXT_VAR);
 }
 
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 long enteredMainTime = clock1000();
 issueBotWarning = earlyBotCheck(enteredMainTime, "hgCustom", delayFraction, 0, 0, "html");
 htmlPushEarlyHandlers();
 oldVars = hashNew(10);
 cgiSpoof(&argc, argv);
 cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars);