a53b9958fa734f73aeffb9ddfe2fbad1ca65f90c
galt
  Mon Jan 30 16:18:41 2017 -0800
Check-in of CSP2 Content-Security-Policy work. All C-language CGIs should now support CSP2 in browser to stop major forms of XSS javascript injection. Javascript on pages is gathered together, and then emitted in a single script block at the end with a nonce that tells the browser, this is js that we generated instead of being injected by a hacker. Both inline script from script blocks and inline js event handlers had to be pulled out and separated. You will not see js sprinkled through-out the page now. Older browsers that support CSP1 or that do not understand CSP at all will still work, just without protection. External js libraries loaded at runtime need to be added to the CSP policy header in src/lib/htmshell.c.

diff --git src/lib/cheapcgi.c src/lib/cheapcgi.c
index ad4a545..fbeea5b 100644
--- src/lib/cheapcgi.c
+++ src/lib/cheapcgi.c
@@ -5,30 +5,230 @@
  * granted for all use - public, private or commercial. */
 
 #include "common.h"
 #include "hash.h"
 #include "cheapcgi.h"
 #include "portable.h"
 #include "linefile.h"
 #include "errAbort.h"
 #include "filePath.h"
 #include "htmshell.h"
 #ifndef GBROWSE
 #include "mime.h"
 #endif /* GBROWSE */
 #include <signal.h>
 
+//============ javascript inline-separation routines ===============
+
+// One of the main services that CSP (Content Security Policy) provides
+// is protecting from reflected and stored XSS attacks by disabling all inline javacript,
+// both in script tags, and in inline event handlers.  The separated javascript 
+// can be either added back to the end of the html page with a nonce or sha hashid,
+// or it can be saved to a temp file in trash and then included as a non-inline, off-page .js.
+
+// TODO make other versions that capture the output to a temp file.
+
+/* OBSOLETE
+// Auto-increment. This helps create unique ids for easily connecting inline js with the html element.
+
+int autoInc = 0;
+int getAutoInc()
+// Get auto-incrementing value.
+{
+return autoInc++;
+}
+
+char *getAutoIncId()
+// Generate an automatically incrementing html id attribute value.
+// For cases where the element is not given any id, generate a unique id automatically. 
+{
+char autoId[32];
+safef(autoId, sizeof autoId, "auto%d", getAutoInc());
+return cloneString(autoId);
+}
+END OBSOLETE */
+
+struct dyString *jsInlineLines = NULL;
+
+void jsInlineInit()
+/* init if needed */
+{
+if (!jsInlineLines) 
+    {
+    jsInlineLines = dyStringNew(1024);	
+    }
+}
+
+void jsInline(char *javascript)
+/* Add javascript text to output file or memory structure */
+{
+jsInlineInit(); // init if needed
+dyStringAppend(jsInlineLines, javascript);
+}
+
+boolean jsInlineFinishCalled = FALSE;
+
+void jsInlineFinish()
+/* finish outputting accumulated inline javascript */
+{
+if (jsInlineFinishCalled)
+    {
+    // jsInlineFinish can be called multiple times when generating framesets or genomeSpace.
+    warn("jsInlineFinish() called already.");  // TODO GALT
+    }
+jsInlineInit(); // init if needed
+printf("<script type='text/javascript' nonce='%s'>\n%s</script>\n", getNonce(), jsInlineLines->string);
+dyStringClear(jsInlineLines);
+jsInlineFinishCalled = TRUE;
+}
+
+void jsInlineReset()
+/* used by genomeSpace to repeatedly output multiple pages to stdout */
+{
+jsInlineFinishCalled = FALSE;
+}
+
+const char * const jsEvents[] = { 
+"abort",
+"activate",
+"afterprint",
+"afterupdate",
+"beforeactivate",
+"beforecopy",
+"beforecut",
+"beforedeactivate",
+"beforeeditfocus",
+"beforepaste",
+"beforeprint",
+"beforeunload",
+"beforeupdate",
+"blur",
+"bounce",
+"cellchange",
+"change",
+"click",
+"contextmenu",
+"controlselect",
+"copy",
+"cut",
+"dataavailable",
+"datasetchanged",
+"datasetcomplete",
+"dblclick",
+"deactivate",
+"drag",
+"dragend",
+"dragenter",
+"dragleave",
+"dragover",
+"dragstart",
+"drop",
+"error",
+"errorupdate",
+"filterchange",
+"finish",
+"focus",
+"focusin",
+"focusout",
+"hashchange",
+"help",
+"input",
+"keydown",
+"keypress",
+"keyup",
+"load",
+"losecapture",
+"message",
+"mousedown",
+"mouseenter",
+"mouseleave",
+"mousemove",
+"mouseout",
+"mouseover",
+"mouseup",
+"mousewheel",
+"move",
+"moveend",
+"movestart",
+"offline",
+"line",
+"online",
+"paste",
+"propertychange",
+"readystatechange",
+"reset",
+"resize",
+"resizeend",
+"resizestart",
+"rowenter",
+"rowexit",
+"rowsdelete",
+"rowsinserted",
+"scroll",
+"search",
+"select",
+"selectionchange",
+"selectstart",
+"start",
+"stop",
+"submit",
+"unload",
+"" };
+
+
+boolean findJsEvent(char *event)
+/* see if it is in the list */
+{
+int i = 0;
+while (TRUE)
+    {
+    const char *w = jsEvents[i];
+    if (sameString(w, event))
+	return TRUE;
+    if (sameString(w, ""))
+	return FALSE;
+    ++i;
+    }
+return FALSE; // should never get here
+}
+
+void checkValidEvent(char *event)
+/* check if it is lowercase and a known valid event name */
+{
+char *temp = cloneString(event);
+tolowers(temp);
+if (!sameString(temp, event))
+    warn("jsInline: javascript event %s should be given in lower-case", event);
+event = temp; 
+if (!findJsEvent(event))
+    warn("jsInline: unknown javascript event %s", event);
+freeMem (event);
+}
+
+void jsOnEventById(char *event, char *idText, char *jsText)
+/* Add js mapping for inline event */
+{
+checkValidEvent(event);
+struct dyString *javascript = dyStringNew(1024);  // TODO XSS Filter the idText?
+dyStringPrintf(javascript, "document.getElementById('%s').on%s = function() {%s};\n", idText, event, jsText);
+jsInline(javascript->string);
+dyStringFree(&javascript);
+}
+
+
+//============ END of javascript inline-separation routines ===============
+
 
 /* These three variables hold the parsed version of cgi variables. */
 static char *inputString = NULL;
 static unsigned long inputSize;
 static struct hash *inputHash = NULL;
 static struct cgiVar *inputList = NULL;
 
 static boolean haveCookiesHash = FALSE;
 static struct hash *cookieHash = NULL;
 static struct cgiVar *cookieList = NULL;
 
 /* should cheapcgi use temp files to store uploaded files */
 static boolean doUseTempFile = FALSE;
 
 void dumpCookieList()
@@ -1265,71 +1465,80 @@
 void cgiMakeSubmitButton()
 /* Make 'submit' type button. */
 {
 cgiMakeButton("Submit", "Submit");
 }
 
 void cgiMakeResetButton()
 /* Make 'reset' type button. */
 {
 printf("<INPUT TYPE=RESET NAME=\"Reset\" VALUE=\" Reset \">");
 }
 
 void cgiMakeClearButton(char *form, char *field)
 /* Make button to clear a text field. */
 {
+char id[256];
+safef(id, sizeof id, "%s_clickBut", form);
 char javascript[1024];
-
 safef(javascript, sizeof(javascript),
     "document.%s.%s.value = ''; document.%s.submit();", form, field, form);
-cgiMakeOnClickButton(javascript, " Clear  ");
+cgiMakeOnClickButton(id, javascript, " Clear  ");
 }
 
 void cgiMakeButtonWithMsg(char *name, char *value, char *msg)
 /* Make 'submit' type button. Display msg on mouseover, if present*/
 {
-printf("<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"%s\" %s%s%s>",
-        name, value,
-        (msg ? " TITLE=\"" : ""), (msg ? msg : ""), (msg ? "\"" : "" ));
+printf("<input type='submit' name='%s' value='%s'",
+        name, value);
+if (msg)
+    printf(" title='%s'", msg);
+printf(">");
 }
 
 void cgiMakeButtonWithOnClick(char *name, char *value, char *msg, char *onClick)
 /* Make 'submit' type button, with onclick javascript */
 {
-printf("<input type=\"submit\" name=\"%s\" value=\"%s\" onclick=\"%s\" %s%s%s>",
-        name, value, onClick,
-        (msg ? " TITLE=\"" : ""), (msg ? msg : ""), (msg ? "\"" : "" ));
+printf("<input type='submit' name='%s' id='%s' value='%s'",
+        name, name, value);
+if (msg)
+    printf(" title='%s'", msg);
+printf(">");
+
+jsOnEventById("click", name, onClick);
 }
 
 void cgiMakeButton(char *name, char *value)
 /* Make 'submit' type button */
 {
 cgiMakeButtonWithMsg(name, value, NULL);
 }
 
-void cgiMakeOnClickButton(char *command, char *value)
-/* Make 'push' type button with client side onClick (java)script. */
+void cgiMakeOnClickButton(char *id, char *command, char *value)
+/* Make button with client side onClick javascript. */
 {
-printf("<INPUT TYPE=\"button\" VALUE=\"%s\" onClick=\"%s\">", value, command);
+printf("<INPUT TYPE='button' id='%s' VALUE=\"%s\">", id, value);
+jsOnEventById("click", id, command);
 }
 
 void cgiMakeOnClickSubmitButton(char *command, char *name, char *value)
 /* Make submit button with both variable name and value with client side
  * onClick (java)script. */
 {
-printf("<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"%s\" onClick=\"%s\">",
-       name, value, command);
+printf("<INPUT TYPE=SUBMIT NAME='%s' id='%s' VALUE=\"%s\">",
+       name, name, value);
+jsOnEventById("click", name, command);
 }
 
 void cgiMakeOptionalButton(char *name, char *value, boolean disabled)
 /* Make 'submit' type button that can be disabled. */
 {
 printf("<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"%s\"", name, value);
 if (disabled)
     printf(" DISABLED");
 printf(">");
 }
 
 void cgiMakeFileEntry(char *name)
 /* Make file entry box/browser */
 {
     printf("<INPUT TYPE=FILE NAME=\"%s\">", name);
@@ -1391,43 +1600,57 @@
         (msg ? " TITLE=\"" : ""), (msg ? msg : ""), (msg ? "\"" : "" ),
         text);
 }
 
 void cgiParagraph(char *text)
 /* Make text paragraph */
 {
 printf("<P> %s\n", text);
 }
 
 void cgiMakeRadioButton(char *name, char *value, boolean checked)
 /* Make radio type button.  A group of radio buttons should have the
  * same name but different values.   The default selection should be
  * sent with checked on. */
 {
-printf("<INPUT TYPE=RADIO NAME=\"%s\" VALUE=\"%s\" %s>", name, value,
-   (checked ? "CHECKED" : ""));
+printf("<input type=radio name='%s' id='%s' value='%s'",
+        name, name, value);
+if (checked)
+   printf(" CHECKED");
+printf(">");
 }
 
-void cgiMakeOnClickRadioButton(char *name, char *value, boolean checked,
-                                        char *command)
-/* Make radio type button with onClick command.
+void cgiMakeOnEventRadioButtonWithClass(char *name, char *value, boolean checked,
+    char *class, char *event, char *command)
+/* Make radio type button with an event and an optional class attribute.
  *  A group of radio buttons should have the
  * same name but different values.   The default selection should be
- * sent with checked on. */
-{
-printf("<INPUT TYPE=RADIO NAME=\"%s\" VALUE=\"%s\" %s %s>",
-        name, value, command, (checked ? "CHECKED" : ""));
+ * sent with checked on. If class is non-null it is included. */
+{
+char temp[256];
+safef(temp, sizeof temp, "%s_%s", name, value);
+char *valNoSpc = replaceChars(temp, " ", "_"); // replace spaces with underscore
+char *id = replaceChars(valNoSpc, ".", "_"); // replace dots with underscore, js does not like dots in ids
+freeMem(valNoSpc);
+printf("<input type=radio name='%s' id='%s' value='%s'",
+        name, id, value);
+if (checked)
+   printf(" CHECKED");
+printf(" class='%s'", class);
+printf(">");
+jsOnEventById(event, id, command);
+freeMem(id);
 }
 
 char *cgiBooleanShadowPrefix()
 /* Prefix for shadow variable set with boolean variables. */
 {
 return "boolshad.";
 }
 #define BOOLSHAD_EXTRA "class='cbShadow'"
 
 boolean cgiBooleanDefined(char *name)
 /* Return TRUE if boolean variable is defined (by
  * checking for shadow. */
 {
 char buf[256];
 safef(buf, sizeof(buf), "%s%s", cgiBooleanShadowPrefix(), name);
@@ -1487,42 +1710,50 @@
 cgiMakeCheckBox2Bool(name, checked, TRUE, id, NULL);
 }
 
 void cgiMakeCheckBox(char *name, boolean checked)
 /* Make check box. */
 {
 cgiMakeCheckBox2Bool(name, checked, TRUE, NULL, NULL);
 }
 
 void cgiMakeCheckBoxEnabled(char *name, boolean checked, boolean enabled)
 /* Make check box, optionally enabled/disabled. */
 {
 cgiMakeCheckBox2Bool(name, checked, enabled, NULL, NULL);
 }
 
+// TODO hopefully make this OBSOLETE
 void cgiMakeCheckBoxJS(char *name, boolean checked, char *javascript)
 /* Make check box with javascript. */
 {
 cgiMakeCheckBox2Bool(name,checked,TRUE,NULL,javascript);
 }
 
+// TODO hopefully make this OBSOLETE
 void cgiMakeCheckBoxIdAndJS(char *name, boolean checked, char *id, char *javascript)
 /* Make check box with ID and javascript. */
 {
 cgiMakeCheckBox2Bool(name,checked,TRUE,id,javascript);
 }
 
+void cgiMakeCheckBoxIdAndMore(char *name, boolean checked, char *id, char *moreHtml)
+/* Make check box with ID and extra (non-javascript) html. */
+{
+cgiMakeCheckBox2Bool(name,checked,TRUE,id,moreHtml);
+}
+
 void cgiMakeCheckBoxFourWay(char *name, boolean checked, boolean enabled, char *id,
                             char *classes, char *moreHtml)
 /* Make check box - with fourWay functionality (checked/unchecked by enabled/disabled)
  * Also makes a shadow hidden variable that supports the 2 boolean states. */
 {
 char shadName[256];
 
 printf("<INPUT TYPE=CHECKBOX NAME='%s'", name);
 if (id)
     printf(" id='%s'", id);
 if (checked)
     printf(" CHECKED");
 if (!enabled)
     {
     if (findWordByDelimiter("disabled",' ', classes) == NULL) // fauxDisabled ?
@@ -1572,59 +1803,58 @@
        rowCount, columnCount, disabled ? "DISABLED" : "",
        (initialVal != NULL ? initialVal : ""));
 }
 
 void cgiMakeOnKeypressTextVar(char *varName, char *initialVal, int charSize,
 			      char *script)
 /* Make a text control filled with initial value, with a (java)script
  * to execute every time a key is pressed.  If charSize is zero it's
  * calculated from initialVal size. */
 {
 if (initialVal == NULL)
     initialVal = "";
 if (charSize == 0) charSize = strlen(initialVal);
 if (charSize == 0) charSize = 8;
 
-htmlPrintf("<INPUT TYPE=TEXT NAME='%s|attr|' SIZE=%d VALUE='%s|attr|'", varName,
-        charSize, initialVal);
+htmlPrintf("<INPUT TYPE=TEXT NAME='%s|attr|' ID='%s|attr|' SIZE=%d VALUE='%s|attr|'", 
+	varName, varName, charSize, initialVal);
 if (isNotEmpty(script))
-    printf(" onkeypress='%s'", script); // TODO XSS
+    jsOnEventById("keypress", varName, script);
 printf(">\n");
 }
 
 void cgiMakeTextVar(char *varName, char *initialVal, int charSize)
 /* Make a text control filled with initial value.  If charSize
  * is zero it's calculated from initialVal size. */
 {
 cgiMakeOnKeypressTextVar(varName, initialVal, charSize, NULL);
 }
 
-void cgiMakeTextVarWithExtraHtml(char *varName, char *initialVal, int width, char *extra)
+void cgiMakeTextVarWithExtraHtml(char *varName, char *initialVal, int width, char *event, char *javascript)
 /* Make a text control filled with initial value. */
 {
 if (initialVal == NULL)
     initialVal = "";
 if (width==0)
     width=strlen(initialVal)*10;
 if (width==0)
     width = 100;
 
-htmlPrintf("<INPUT TYPE=TEXT class='inputBox' NAME='%s|attr|' style='width:%dpx' VALUE='%s|attr|'",
-       varName, width, initialVal);
-if (isNotEmpty(extra))
-    printf(" %s",extra); // TODO XSS
-printf(">\n");
+htmlPrintf("<INPUT TYPE=TEXT class='inputBox' NAME='%s|attr|' id='%s' style='width:%dpx' VALUE='%s|attr|'>\n",
+       varName, varName, width, initialVal);
+if (event)
+    jsOnEventById(event, varName, javascript);
 }
 
 void cgiMakeIntVarWithExtra(char *varName, int initialVal, int maxDigits, char *extra)
 /* Make a text control filled with initial value and optional extra HTML.  */
 {
 if (maxDigits == 0) maxDigits = 4;
 htmlPrintf("<INPUT TYPE=TEXT NAME='%s|attr|' SIZE=%d VALUE=%d %s|none|>", // TODO XSS extra
                 varName, maxDigits, initialVal, extra ? extra : "");
 }
 
 void cgiMakeIntVar(char *varName, int initialVal, int maxDigits)
 /* Make a text control filled with initial value.  */
 {
 cgiMakeIntVarWithExtra(varName, initialVal, maxDigits, NULL);
 }
@@ -1640,34 +1870,36 @@
     if (max)
         width=strlen(max)*10;
     else
         {
         int sz=initialVal+1000;
         if (min)
             sz=atoi(min) + 1000;
         width = 10;
         while (sz/=10)
             width+=10;
         }
     }
 if (width < 65)
     width = 65;
 
-printf("<INPUT TYPE=TEXT class='inputBox' name=\"%s\" style='width: %dpx' value=%d",
-       varName,width,initialVal);
-printf(" onChange='return validateInt(this,%s,%s);'",
+printf("<INPUT TYPE=TEXT class='inputBox' name='%s' id='%s' style='width: %dpx' value=%d",
+       varName,varName,width,initialVal);
+char javascript[1024];
+safef(javascript, sizeof javascript, "return validateInt(this,%s,%s);",
        (min ? min : "\"null\""),(max ? max : "\"null\""));
+jsOnEventById("change", varName, javascript);
 if (title)
     printf(" title='%s'",title);
 printf(">\n");
 }
 
 void cgiMakeIntVarWithLimits(char *varName, int initialVal, char *title, int width,
                              int min, int max)
 {
 char minLimit[20];
 char maxLimit[20];
 char *minStr=NULL;
 char *maxStr=NULL;
 if (min != NO_VALUE)
     {
     safef(minLimit,sizeof(minLimit),"%d",min);
@@ -1716,34 +1948,36 @@
 
 void cgiMakeDoubleVarInRange(char *varName, double initialVal, char *title, int width,
                              char *min, char *max)
 /* Make a floating point control filled with initial value.
    If min and/or max are non-NULL will enforce range
    Requires utils.js jQuery.js and inputBox class */
 {
 if (width==0)
     {
     if (max)
         width=strlen(max)*10;
     }
 if (width < 65)
     width = 65;
 
-printf("<INPUT TYPE=TEXT class='inputBox' name=\"%s\" style='width: %dpx' value=%g",
-       varName,width,initialVal);
-printf(" onChange='return validateFloat(this,%s,%s);'",
+printf("<INPUT TYPE=TEXT class='inputBox' name='%s' id='%s' style='width: %dpx' value=%g",
+       varName,varName,width,initialVal);
+char javascript[1024];
+safef(javascript, sizeof javascript, "return validateFloat(this,%s,%s);",
        (min ? min : "\"null\""),(max ? max : "\"null\""));
+jsOnEventById("change", varName, javascript);
 if (title)
     printf(" title='%s'",title);
 printf(">\n");
 }
 
 void cgiMakeDoubleVarWithLimits(char *varName, double initialVal, char *title, int width,
                                 double min, double max)
 {
 char minLimit[20];
 char maxLimit[20];
 char *minStr=NULL;
 char *maxStr=NULL;
 if ((int)min != NO_VALUE)
     {
     safef(minLimit,sizeof(minLimit),"%g",min);
@@ -1770,62 +2004,69 @@
 }
 
 void cgiMakeDoubleVarWithMax(char *varName, double initialVal, char *title, int width, double max)
 {
 char maxLimit[20];
 char *maxStr=NULL;
 if ((int)max != NO_VALUE)
     {
     safef(maxLimit,sizeof(maxLimit),"%g",max);
     maxStr = maxLimit;
     }
 cgiMakeDoubleVarInRange(varName,initialVal,title,width,NULL,maxStr);
 }
 
 void cgiMakeDropListClassWithStyleAndJavascript(char *name, char *menu[],
-        int menuSize, char *checked, char *class, char *style,char *javascript)
+        int menuSize, char *checked, char *class, char *style, struct slPair *events)
 /* Make a drop-down list with names, text class, style and javascript. */
 {
 int i;
 char *selString;
 if (checked == NULL) checked = menu[0];
 printf("<SELECT");
 if (name)
     printf(" NAME='%s'", name);
 if (class)
     printf(" class='%s'", class);
+if (events)
+    {
+    printf(" id='%s'", name);
+    struct slPair *e;
+    for(e = events; e; e = e->next)
+	{
+	jsOnEventById(e->name, name, e->val);
+	}    
+    }
 if (style)
     printf(" style='%s'", style);
-if (javascript)
-    printf(" %s", javascript);
 printf(">\n");
 for (i=0; i<menuSize; ++i)
     {
     if (sameWord(menu[i], checked))
         selString = " SELECTED";
     else
         selString = "";
     printf("<OPTION%s>%s</OPTION>\n", selString, menu[i]);
     }
 printf("</SELECT>\n");
 }
 
 void cgiMakeDropListClassWithStyle(char *name, char *menu[],
                                    int menuSize, char *checked, char *class, char *style)
 /* Make a drop-down list with names, text class and style. */
 {
-cgiMakeDropListClassWithStyleAndJavascript(name,menu,menuSize,checked,class,style,"");
+cgiMakeDropListClassWithStyleAndJavascript(name,menu,menuSize,checked,class,style,NULL);
 }
 
 void cgiMakeDropListClass(char *name, char *menu[],
                           int menuSize, char *checked, char *class)
 /* Make a drop-down list with names. */
 {
 cgiMakeDropListClassWithStyle(name, menu, menuSize, checked, class, NULL);
 }
 
 void cgiMakeDropList(char *name, char *menu[], int menuSize, char *checked)
 /* Make a drop-down list with names.
  * uses style "normalText" */
 {
     cgiMakeDropListClass(name, menu, menuSize, checked, "normalText");
 }
@@ -1882,83 +2123,116 @@
 	printf("<TD></TD>");
 puts("</TR></TABLE>");
 char buf[512];
 safef(buf, sizeof(buf), "%s%s", cgiMultListShadowPrefix(), name);
 cgiMakeHiddenVar(buf, "0");
 }
 
 void cgiMakeCheckboxGroup(char *name, char *menu[], int menuSize, struct slName *checked,
 			  int tableColumns)
 /* Make a table of checkboxes that have the same variable name but different
  * values (same behavior as a multi-select input). */
 {
 cgiMakeCheckboxGroupWithVals(name, menu, NULL, menuSize, checked, tableColumns);
 }
 
-void cgiMakeDropListFull(char *name, char *menu[], char *values[],
-                         int menuSize, char *checked, char *extraAttribs)
-/* Make a drop-down list with names and values. */
+void cgiMakeDropListFullExt(char *name, char *menu[], char *values[],
+                         int menuSize, char *checked, char *event, char *javascript, char *style, char *class)
+/* Make a drop-down list with names and values.
+ * Optionally include values for style and class */
 {
 int i;
 char *selString;
 if (checked == NULL) checked = menu[0];
 
-if (NULL != extraAttribs)
-    {
-    printf("<SELECT NAME=\"%s\" %s>\n", name, extraAttribs);
-    }
-else
+printf("<SELECT NAME='%s'", name);
+if (class)
+    printf(" class='%s'", class);
+if (javascript)
     {
-    printf("<SELECT NAME=\"%s\">\n", name);
+    printf(" id='%s'", name);
+    jsOnEventById(event, name, javascript);
     }
+if (style)
+    printf(" style='%s'", style);
+printf(">\n");
 
 for (i=0; i<menuSize; ++i)
     {
     if (sameWord(values[i], checked))
         selString = " SELECTED";
     else
         selString = "";
     printf("<OPTION%s VALUE=\"%s\">%s</OPTION>\n", selString, values[i], menu[i]);
     }
 printf("</SELECT>\n");
 }
 
+void cgiMakeDropListFull(char *name, char *menu[], char *values[],
+                         int menuSize, char *checked, char *event, char *javascript)
+/* Make a table of checkboxes that have the same variable name but different
+ * values (same behavior as a multi-select input). */
+{
+cgiMakeDropListFullExt(name, menu, values, menuSize, checked, event, javascript, NULL, NULL);
+}
+
 char *cgiMakeSelectDropList(boolean multiple, char *name, struct slPair *valsAndLabels,
-                            char *selected, char *anyAll,char *extraClasses, char *extraHtml)
+                            char *selected, char *anyAll,char *extraClasses, char *event, char *javascript, char *style, char *id)
 // Returns allocated string of HTML defining a drop-down select
 // (if multiple, REQUIRES ui-dropdownchecklist.js)
 // valsAndLabels: val (pair->name) must be filled in but label (pair->val) may be NULL.
 // selected: if not NULL is a val found in the valsAndLabels (multiple then comma delimited list).
 //           If null and anyAll not NULL, that will be selected
 // anyAll: if not NULL is the string for an initial option. It can contain val and label,
 //         delimited by a comma
-// extraHtml: if not NULL contains id, javascript calls and style.
-//            It does NOT contain class definitions
+// event: click, etc.
+// javacript: what to execute when the event happens.
+// style: inline style
+// id is optional
 {
 struct dyString *output = dyStringNew(1024);
 boolean checked = FALSE;
 
 dyStringPrintf(output,"<SELECT name='%s'",name);
 if (multiple)
     dyStringAppend(output," MULTIPLE");
 if (extraClasses != NULL)
     dyStringPrintf(output," class='%s%s'",extraClasses,(multiple ? " filterBy" : ""));
 else if (multiple)
     dyStringAppend(output," class='filterBy'");
 
-if (extraHtml != NULL)
-    dyStringPrintf(output," %s",extraHtml);
+char *autoId = NULL;
+if (javascript)
+    {
+    if (!id)
+	{
+    	id = name;
+	}
+    }
+if (id)
+    dyStringPrintf(output, " id='%s'", id);
+if (javascript)
+    {
+    jsOnEventById(event, id, javascript);
+    freeMem(autoId);
+    }
+
+if (style)
+    {
+    dyStringPrintf(output, " style='%s'", style);
+    }
+
 dyStringAppend(output,">\n");
 
 // Handle initial option "Any" or "All"
 if (anyAll != NULL)
     {
     char *val = anyAll;  // Could contain a label after the value
     char *label = strchr(val,',');  // Could contain a label after the value
     if (label != NULL)
         {
         val = cloneString(anyAll);
         label = strchr(val,',');  // again because this is new mem
         *label = '\0';
         label = label+1;
         }
     else