e6b0dc7877ae4b24f56682885e03263fd2da3582
braney
  Tue Aug 18 11:37:04 2026 -0700
lib, hgc: encode barChart, BAM and VCF detail text consistently, refs #38123

diff --git src/hg/hgc/vcfClick.c src/hg/hgc/vcfClick.c
index eaabf39d65d..aa07282f957 100644
--- src/hg/hgc/vcfClick.c
+++ src/hg/hgc/vcfClick.c
@@ -1,27 +1,28 @@
 /* vcfTrack -- handlers for Variant Call Format data. */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "dystring.h"
 #include "errCatch.h"
 #include "hCommon.h"
 #include "hdb.h"
 #include "hgc.h"
 #include "htmshell.h"
+#include "htmshell.h"
 #include "jsHelper.h"
 #include "pgSnp.h"
 #include "regexHelper.h"
 #include "trashDir.h"
 #include "knetUdc.h"
 #include "udc.h"
 #include "vcf.h"
 #include "vcfUi.h"
 #include "trackHub.h"
 #include "featureBits.h"
 
 #define NA "<em>n/a</em>"
 
 static void printKeysWithDescriptions(struct vcfFile *vcff, int wordCount, char **words,
 				      struct vcfInfoDef *infoDefs, boolean stripToSymbol)
@@ -90,148 +91,168 @@
     printf("<B>Filter:</B> "NA"<BR>\n");
 else if (rec->filterCount == 1 && sameString(rec->filters[0], "PASS"))
     printf("<B>Filter:</B> PASS<BR>\n");
 else
     {
     printf("<B>Filter failures:</B> ");
     printf("<font style='font-weight: bold; color: #FF0000;'>\n");
     struct vcfFile *vcff = rec->file;
     printKeysWithDescriptions(vcff, rec->filterCount, rec->filters, vcff->filterDefs, FALSE);
     printf("</font>\n");
     }
 }
 
 
 
-static int printTabularHeaderRow(const struct vcfInfoDef *def)
+static int printTabularHeaderRow(struct trackDb *tdb, const struct vcfInfoDef *def)
 /* Parse the column header parts out of def->description and print as table header row;
  * call this only when looksTabular returns TRUE.
  * Returns the number of columns in the header */
 {
 regmatch_t substrArr[PATH_LEN];
 if (regexMatchSubstr(def->description, COL_DESC_REGEX, substrArr, ArraySize(substrArr)))
     {
     puts("<TR>");
     // Make a copy of the part of def->description that matches the regex,
     // then chop by '|' and print out header column tags:
     int matchSize = substrArr[0].rm_eo - substrArr[0].rm_so;
     char copy[matchSize+1];
     safencpy(copy, sizeof(copy), def->description + substrArr[0].rm_so, matchSize);
     // Turn '_' into ' ' so description words can wrap inside headers, saving some space
     subChar(copy, '_', ' ');
     char *words[PATH_LEN];
     int descColCount = chopByChar(copy, '|', words, ArraySize(words));
     int i;
     for (i = 0;  i < descColCount; i++)
-        printf("<TH class='withThinBorder'>%s</TH>", words[i]);
+        printf("<TH class='withThinBorder'>%s</TH>", hubEncode(tdb, words[i]));
     puts("</TR>");
     return descColCount;
     }
 else
     errAbort("printTabularHeaderRow: code bug, if looksTabular returns true then "
              "regex should work here");
 return -1;
 }
 
-static void printTabularData(struct vcfInfoElement *el, int headerCount)
+static void printTabularData(struct trackDb *tdb, struct vcfInfoElement *el, int headerCount)
 /* Print a row for each value in el, separating columns by '|'. */
 {
 int j;
 for (j = 0;  j < el->count;  j++)
     {
     puts("<TR>");
     char *val = el->values[j].datString;
     if (!isEmpty(val))
         {
         int len = strlen(val);
         char copy[len+1];
         safencpy(copy, sizeof(copy), val, len);
         char *words[PATH_LEN];
         chopByChar(copy, '|', words, ArraySize(words));
         int k;
         // printTabularHeaderRow strips off (but still prints!) a trailing '|'
         // because of the regex, so enforce that here too so the rows after
         // the header don't get all out of whack
         for (k = 0;  k < headerCount;  k++)
-            printf("<TD class='withThinBorder'>%s</TD>", words[k]);
+            printf("<TD class='withThinBorder'>%s</TD>", hubEncode(tdb, words[k]));
         }
     puts("</TR>");
     }
 }
 
 
-static void vcfInfoDetails(struct vcfRecord *rec, char *trackName, int recordCount)
+static void printInfoDatum(struct trackDb *tdb, const union vcfDatum datum,
+                           const enum vcfInfoType type)
+/* Print one INFO value.  Same as vcfPrintDatum, except that the string forms are escaped for
+ * a hub track, where the VCF file was written by a stranger. */
+{
+if (type == vcfInfoString || type == vcfInfoFlag)
+    {
+    char *val = hubEncode(tdb, datum.datString);
+    if (startsWith("http", datum.datString))
+        printf("<a target=_blank href='%s'>%s</a>", val, val);
+    else
+        printf("%s", val);
+    }
+else
+    vcfPrintDatum(stdout, datum, type);
+}
+
+static void vcfInfoDetails(struct vcfRecord *rec, struct trackDb *tdb, int recordCount)
 /* Expand info keys to descriptions, then print out keys and values. */
 {
+char *trackName = tdb->track;
 if (rec->infoCount == 0)
     return;
 struct vcfFile *vcff = rec->file;
 puts("<table>"); // wrapper table for collapsible section
 char infoId[32];
 safef(infoId, sizeof(infoId), "infoFields%d", recordCount);
 jsBeginCollapsibleSectionFontSize(cart, trackName, infoId, "INFO column annotations:", FALSE, "medium");
 puts("<TABLE class=\"stdTbl\">\n");
 int i;
 for (i = 0;  i < rec->infoCount;  i++)
     {
     struct vcfInfoElement *el = &(rec->infoElements[i]);
     const struct vcfInfoDef *def = vcfInfoDefForKey(vcff, el->key);
+    // the INFO key, its description and its values all come from the VCF file, which for a
+    // hub is a stranger's file
     printf("<TR valign='top'><TD align=\"right\"><B>%s:</B></TD><TD>",
-           el->key);
+           hubEncode(tdb, el->key));
     int j;
     enum vcfInfoType type = def ? def->type : vcfInfoString;
     if (type == vcfInfoFlag && el->count == 0)
 	printf("Yes"); // no values, so we can't call vcfPrintDatum...
     // However, if this is older VCF, type vcfInfoFlag might have a value.
     if (looksTabular(def, el))
         {
         // Make a special display below
         printf("<em>see below</em>");
         }
     else
         {
         for (j = 0;  j < el->count;  j++)
             {
             if (j > 0)
                 printf(", ");
             if (el->missingData[j])
                 printf(".");
             else
-                vcfPrintDatum(stdout, el->values[j], type);
+                printInfoDatum(tdb, el->values[j], type);
             }
         }
     if (def != NULL && !looksTabular(def, el))
-	printf("&nbsp;&nbsp;</TD><TD>%s", def->description);
+	printf("&nbsp;&nbsp;</TD><TD>%s", hubEncode(tdb, def->description));
     else
 	printf("</TD><TD>");
     printf("</TD></TR>\n");
     }
 puts("</TABLE>");
 jsEndCollapsibleSection();
 puts("</table>"); // close the wrapper around the collapsible section
 // Now show the tabular fields, if any
 for (i = 0;  i < rec->infoCount;  i++)
     {
     struct vcfInfoElement *el = &(rec->infoElements[i]);
     const struct vcfInfoDef *def = vcfInfoDefForKey(vcff, el->key);
     if (looksTabular(def, el))
         {
         puts("<BR>");
-        printf("<B>%s</B>: %s<BR>\n", el->key, def->description);
+        printf("<B>%s</B>: %s<BR>\n", hubEncode(tdb, el->key), hubEncode(tdb, def->description));
         puts("<TABLE class='stdTbl'>");
-        int headerCount = printTabularHeaderRow(def);
-        printTabularData(el, headerCount);
+        int headerCount = printTabularHeaderRow(tdb, def);
+        printTabularData(tdb, el, headerCount);
         puts("</TABLE>");
         }
     }
 }
 
 struct sampleMeta
 /* Metadata columns for one sample, loaded from sampleMetadataFile. */
     {
     char **values;      /* Array of column values */
     };
 
 static void loadSampleMetadata(struct trackDb *tdb, struct hash **retHash,
                                 char ***retColNames, int *retColCount)
 /* Load sample metadata from file specified in trackDb setting sampleMetadataFile.
  * File format: tab-separated, first line is header starting with #sample.
@@ -552,31 +573,32 @@
     if (showLeftBase)
 	dyStringPrintf(dy, "(%c)", leftBase);
     abbreviateLongSeq(rec->alleles[i], endLength, showLength, dy);
     if (encodeHtml)
 	displayAls[i] = htmlEncode(dy->string);
     else
 	displayAls[i] = cloneString(dy->string);
     }
 }
 
 static void vcfRecordDetails(struct trackDb *tdb, struct vcfRecord *rec, int recordCount)
 /* Display the contents of a single line of VCF, assumed to be from seqName
  * (using seqName instead of rec->chrom because rec->chrom might lack "chr"). */
 {
 if (isNotEmpty(rec->name) && differentString(rec->name, "."))
-    printf("<B>Name:</B> %s<BR>\n", rec->name);
+    // the ID column comes from the VCF file, a hub's is a stranger's file
+    printf("<B>Name:</B> %s<BR>\n", hubEncode(tdb, rec->name));
 // Add some special URL substitution variables for ExAC/GnomAD-style links
 struct slPair *substFields = slPairNew("ref", rec->alleles[0]);
 substFields->next = slPairNew("firstAlt", rec->alleles[1]);
 char posString[64];
 safef(posString, sizeof posString, "%d", rec->chromStart+1);
 substFields->next->next = slPairNew("pos", posString);
 char *label = rec->name;
 if ((isEmpty(rec->name) || sameString(rec->name, ".")) &&
     (startsWith("exac", tdb->track) || startsWith("gnomad", tdb->track)))
     {
     struct dyString *dyLabel = dyStringCreate("%s-%s-%s-%s", skipChr(rec->chrom), posString,
                                               rec->alleles[0], rec->alleles[1]);
     label = dyStringCannibalize(&dyLabel);
     }
 printCustomUrlWithFields(tdb, rec->name, label, TRUE, substFields);
@@ -609,31 +631,31 @@
                               seqName, rec->chromStart, formName);
         printf("</TABLE></FORM>\n");
         }
     }
 char leftBase = rec->alleles[0][0];
 unsigned int vcfStart = vcfRecordTrimIndelLeftBase(rec);
 boolean showLeftBase = (rec->chromStart == vcfStart+1);
 (void)vcfRecordTrimAllelesRight(rec);
 char *displayAls[rec->alleleCount];
 makeDisplayAlleles(rec, showLeftBase, leftBase, 20, TRUE, FALSE, displayAls);
 printPosOnChrom(seqName, rec->chromStart, rec->chromEnd, NULL, FALSE, rec->name);
 printf("<B>Reference allele:</B> %s<BR>\n", displayAls[0]);
 vcfAltAlleleDetails(rec, displayAls);
 vcfQualDetails(rec);
 vcfFilterDetails(rec);
-vcfInfoDetails(rec, tdb->track, recordCount);
+vcfInfoDetails(rec, tdb, recordCount);
 pgSnpCodingDetail(rec);
 makeDisplayAlleles(rec, showLeftBase, leftBase, 5, FALSE, TRUE, displayAls);
 vcfGenotypesDetails(rec, tdb, displayAls);
 }
 
 void doVcfDetailsCore(struct trackDb *tdb, char *fileOrUrl, boolean isTabix, struct featureBits **pFbList, int rgnStart, int rgnEnd)
 /* Show item details using fileOrUrl. */
 {
 if (!pFbList)
     genericHeader(tdb, NULL);
 int start;
 int end;
 if (pFbList)
     {
     start = rgnStart;