cf3880c50c550e4deb61c9664f4f994e13ab6dd8
max
  Mon Aug 17 08:05:58 2026 -0700
hgc/hgTracks: fix three items from the v502 code review, refs #38106

Shared BLAT alignment links (hgc g=htcBlatAlign) drew the page and then
exit(0), which skips main's cartCheckout, so the loaded session was never
written back and the recipient's cart ended up nearly empty. Add
cartCheckout(&cart) before all four exit(0) calls that follow webEndGb(),
including the two early error returns.

squishCodonFont() decided FreeType was active with a looser test than
maybeNewFonts(), missing the requirement that the font name match a row in
freeTypeFonts[]; when they disagreed the bitmap engine got a cell height it
cannot render. Factor the engine decision into freeTypeFontActive() and use
it in both places.

Query name, chrom and a hub's organism string went raw into single-quoted JS
string literals and page titles on the modern alignment page; the shared-link
path reads the query name back out of a bigPsl, so it is not covered by
hgBlat's whitelist. javaScriptLiteralEncode the JS arguments and htmlEncode
the HTML labels.

Also add "u"/"s" (the shared-link session selectors) to hgc excludeVars so
they are not left in the reader's cart.

diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c
index 8f9d8dd4998..718a3979520 100644
--- src/hg/hgc/hgc.c
+++ src/hg/hgc/hgc.c
@@ -9062,30 +9062,39 @@
 
 static void showSomeAlignmentModern(struct psl *psl, bioSeq *oSeq, enum gfType qType,
                        int qStart, int qEnd, char *qName, int cdsS, int cdsE, boolean blatContext)
 /* Modern single-page version of showSomeAlignment: a gold title bar, a full-height "jump to"
  * sidebar, then an "Alignment summary" and the base-by-base alignment inlined below with steel-blue
  * section headers, so the whole page scrolls (no <frameset>).  The alignment body itself is
  * generated by the shared library as before.  The caller supplies the page chrome via
  * webStartGbNoBanner()/webEndGb() - a menubar and <main> with no legacy section tables - so
  * everything here is plain, table-free HTML.  blatContext=TRUE is the hgBlat path: the title says
  * "BLAT" and the bar carries "Back to results" and "Share a link"; blatContext=FALSE is a plain
  * track click (mRNA/EST/PSL...), which has no BLAT results to go back to or share. */
 {
 if (qName == NULL)
     qName = psl->qName;
 char *chrom = chromAliasGetDisplayChrom(database, cart, psl->tName);
+/* For a shared link (htcBlatAlign) qName is read back out of a bigPsl and chrom/tName can be
+ * hub-supplied, so escape them before they go into the HTML summary and the JS string literals
+ * below rather than trusting the caller's sanitizing (hgBlat whitelists fresh-search query names,
+ * but the shared-link path does not run through that). */
+char *qNameHtml = htmlEncode(qName);
+char *chromHtml = htmlEncode(chrom);
+char *qNameJs = javaScriptLiteralEncode(qName);
+char *chromJs = javaScriptLiteralEncode(chrom);
+char *tNameJs = javaScriptLiteralEncode(psl->tName);
 /* Alternate (chromAlias) names for the genomic sequence - e.g. its RefSeq/GenBank/Ensembl accessions
  * - shown after the main name in the "Only genome sequence" header. */
 struct dyString *aliasDy = dyStringNew(128);
 struct slName *aliasList = chromAliasFindAliases(psl->tName), *al;
 struct hash *seenAlias = hashNew(0);
 hashStore(seenAlias, chrom);        /* skip the name already shown, and the native name */
 hashStore(seenAlias, psl->tName);
 boolean firstAlias = TRUE;
 for (al = aliasList; al != NULL; al = al->next)
     {
     if (isEmpty(al->name) || hashLookup(seenAlias, al->name))
         continue;   /* skip empties, the shown name, and duplicates (e.g. Ensembl and GenBank "7") */
     hashStore(seenAlias, al->name);
     dyStringPrintf(aliasDy, "%s%s", firstAlias ? "" : ", ", al->name);
     firstAlias = FALSE;
@@ -9144,32 +9153,36 @@
        "#blatAlnContent pre{margin:0; padding:2px 0 12px; line-height:1.4; background:none; border:0;"
        " border-radius:0; color:#374a5e; white-space:pre; word-break:normal; word-wrap:normal}"
        /* key-value summary strip, mirroring hgBlat's .blatStrip (label over value, thin dividers) */
        ".blatAlnStrip{display:flex; align-items:center; gap:24px; flex-wrap:wrap; margin:2px 0 14px}"
        ".blatAlnStat{display:flex; flex-direction:column; gap:1px}"
        ".blatAlnStat .k{font-size:12px; color:#5b6572; font-weight:700}"
        ".blatAlnStat .v{font-size:14px; color:#1e2833; font-weight:700}"
        ".blatAlnStrip .d{width:1px; height:28px; background:#d0d0d0}"
        "</style>\n");
 
 /* gold title bar, drawn directly (no framework subheadingBar, no JS): title on the left, then (in
  * the hgBlat context) a "Back to results" and, when a durable track backs the results, a "Share a
  * link" button; in a plain track click, a "Back to Genome Browser" button that returns to hgTracks
  * at this alignment's location. */
 printf("<div class='blatTitleBar'>");
+char *asmLabel = blatAsmLabel(database);   // may carry a hub's genomes.txt organism string
+char *asmLabelHtml = htmlEncode(asmLabel);
 printf("<span class='blatTtl'>%sBase Alignment: %s</span>",
-       blatContext ? "BLAT " : "", blatAsmLabel(database));
+       blatContext ? "BLAT " : "", asmLabelHtml);
+freeMem(asmLabelHtml);
+freeMem(asmLabel);
 printf("<span class='blatBtns'>");
 if (blatContext)
     printf("<a href='hgBlat?blatReopen=1&hgsid=%s' class='blatBtn'>"
            "\xe2\x80\xb9 Back to results</a>", cartSessionId(cart));
 else
     printf("<a href='hgTracks?db=%s&position=%s:%d-%d&hgsid=%s' class='blatBtn'>"
            "Back to Genome Browser \xe2\x80\xba</a>",
            database, psl->tName, psl->tStart + 1, psl->tEnd, cartSessionId(cart));
 if (canShare)
     printf("<a href='#' id='blatShareBtn' class='blatBtn'>Share a link</a>");
 printf("</span></div>\n");
 
 /* one white panel laid out as two grid columns: a full-height "jump to" sidebar on the left, and on
  * the right an "Alignment Summary" header, the summary line, and the base-by-base alignment inlined
  * so the whole page scrolls */
@@ -9186,33 +9199,37 @@
 sprintLongWithCommas(qSizeC, psl->qSize);
 /* key-value strip (Query / Position / Identity / Matches / Strand), styled like hgBlat's summary
  * strip so the two pages read as one design. */
 printf("<div class='blatAlnStrip'>"
        "<div class='blatAlnStat'><span class='k'>Query</span><span class='v'>%s</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Position</span><span class='v'>%s:%s-%s</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Identity</span>"
        "<span class='v' style='color:%s'>%.1f%%</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Matches</span><span class='v'>%s of %s</span></div>"
        "<div class='d'></div>"
        "<div class='blatAlnStat'><span class='k'>Strand</span><span class='v'>%s</span></div>"
        "</div>\n",
-       qName, chrom, tStartC, tEndC, idColor, ident, matchC, qSizeC, psl->strand);
+       qNameHtml, chromHtml, tStartC, tEndC, idColor, ident, matchC, qSizeC, psl->strand);
 if (isNotEmpty(aliasStr))
-    printf("<p>Genome sequence %s is also known as: %s.</p>\n", chrom, aliasStr);
+    {
+    char *aliasStrHtml = htmlEncode(aliasStr);
+    printf("<p>Genome sequence %s is also known as: %s.</p>\n", chromHtml, aliasStrHtml);
+    freeMem(aliasStrHtml);
+    }
 /* The shared library returns the number of alignment blocks it actually shows.  The DNA path merges
  * blocks separated by gaps <= 8 bases, so this can be fewer than psl->blockCount; use it (not
  * psl->blockCount) so the sidebar's "Block N" links match the #1..#N anchors that were emitted. */
 int blockCount;
 /* Capture the shared library's alignment HTML so we can reorder its sections for this page.  The
  * library emits them as Query (#cDNA), Genome (#genomic), then Side-by-side (#ali), with the
  * per-block anchors living inside the Genome section.  We want Query, Side-by-side, Genome so the
  * long per-block list sits at the bottom of both the page and the sidebar.  Reorder here, on the
  * server, rather than in JS, so the page does not reflow after it loads. */
 char *alnHtml = NULL;
 size_t alnLen = 0;
 FILE *alnF = open_memstream(&alnHtml, &alnLen);
 if (alnF == NULL)
     {
     /* open_memstream failed (out of memory): render straight to stdout, skipping the section
@@ -9252,78 +9269,85 @@
 if (blockCount > 1)   /* per-block jump links, indented under the genome-sequence item where their anchors live */
     {
     int bi;
     printf("<div id='blatAlnBlocks'>\n");
     for (bi = 1;  bi <= blockCount;  ++bi)
         printf("<a href='#%d'>Block %d</a>\n", bi, bi);
     printf("</div>\n");
     }
 printf("</div></div>\n");
 
 printf("</div>\n");   /* #blatAlnBody */
 
 /* The cDNA/Genomic/Side-by-side section headers come from shared library code (fuzzyShow.c /
  * pslShow.c) as "cDNA <qName>" / "Genomic <chrom> :" / "Side by Side Alignment"; relabel them to
  * the sidebar wording (sentence case) via JS (there is no C hook for it), keeping the
- * #cDNA/#genomic/#ali jump anchors.  qName and chrom are already sanitized. */
+ * #cDNA/#genomic/#ali jump anchors.  qName and chrom are escaped for a JS string literal
+ * (qNameJs/chromJs) since a shared link's query name is not otherwise sanitized. */
 jsInlineF(
     "(function(){\n"
     "function relabel(anchor, text){\n"
     "  var a = document.getElementsByName(anchor);\n"
     "  if (a && a.length){\n"
     "    var h = a[0].parentNode;\n"
     "    var star = /\\*\\s*$/.test(h.textContent) ? '*' : '';\n"  // keep the footnote marker if present
     "    h.textContent = '';\n"
     "    var k = document.createElement('a'); k.name = anchor; h.appendChild(k);\n"
     "    h.appendChild(document.createTextNode(text + star));\n"
     "  }\n"
     "}\n"
     "relabel('cDNA', 'Only query sequence: %s');\n"
     "relabel('genomic', 'Only genome sequence: %s');\n"
     "relabel('ali', 'Side by side alignment');\n"   // match the sidebar wording and sentence case
     "})();\n",
-    qName, chrom);
+    qNameJs, chromJs);
 
 /* "Share a link": save an anonymous session (hgSession API), build a durable hgc?g=htcBlatAlign link
  * that rebuilds THIS alignment from the session's durable bigPsl custom track (no BLAT re-run, no
  * stored trash sequence), and hand it to the shared "Share a link" modal (topLinks.js shareUrl,
- * loaded by the menu bar) so it looks like every other share dialog.  qName is already sanitized. */
+ * loaded by the menu bar) so it looks like every other share dialog.  tName and qName are escaped
+ * for the single-quoted JS string literals they land in (tNameJs/qNameJs). */
 if (canShare)
     jsInlineF(
     "(function(){\n"
     "var btn = document.getElementById('blatShareBtn');\n"
     "if (!btn) return;\n"
     "btn.addEventListener('click', function(ev){\n"
     "  ev.preventDefault();\n"
     "  if (btn.dataset.busy) return;\n"
     "  btn.dataset.busy = '1';\n"
     "  var label = btn.textContent;\n"
     "  btn.textContent = 'Creating link\\u2026';\n"
     "  fetch('../cgi-bin/hgSession', {method:'POST', credentials:'same-origin',"
     " headers:{'Content-Type':'application/x-www-form-urlencoded'},"
     " body:'hgsid=%s&hgS_doSaveSessionJson=1&hgS_shareAnon=1'})\n"
     "  .then(function(r){ return r.json(); }).then(function(data){\n"
     "    btn.textContent = label; btn.dataset.busy = '';\n"
     "    if (!data || !data.name) return;\n"
     "    var link = window.location.origin + window.location.pathname +\n"
     "      '?g=htcBlatAlign&c=%s&o=%d&i=' + encodeURIComponent('%s') +\n"  // db comes from the session
     "      '&u=l&s=' + encodeURIComponent(data.name);\n"
     "    if (window.topLinks && topLinks.shareUrl) topLinks.shareUrl(link);\n"
     "  }).catch(function(){ btn.textContent = label; btn.dataset.busy = ''; });\n"
     "});\n"
     "})();\n",
-    cartSessionId(cart), psl->tName, psl->tStart, qName);
+    cartSessionId(cart), tNameJs, psl->tStart, qNameJs);
+freeMem(qNameHtml);
+freeMem(chromHtml);
+freeMem(qNameJs);
+freeMem(chromJs);
+freeMem(tNameJs);
 }
 
 void alnModernStart(char *classicTitle)
 /* Begin an alignment page.  With the modernAlignPage hg.conf flag set, start the modern single-page
  * chrome (webStartGbNoBanner) and arm gAlnModern so showSomeAlignment() renders the modern body;
  * otherwise start the classic <frameset>.  This is the plain track-click entry point (mRNA/EST/PSL
  * details, transMap, retrogene, literature alignments), so the modern page is drawn in its neutral,
  * non-BLAT form.  Callers must pair it with showSomeAlignment(), which honors gAlnModern. */
 {
 if (cfgOptionBooleanDefault("modernAlignPage", FALSE))
     {
     gAlnModern = TRUE;
     char pageTitle[256];
     safef(pageTitle, sizeof pageTitle, "Base Alignment: %s", blatAsmLabel(database));
     webStartGbNoBanner(cart, database, pageTitle);   // menubar + <main>, no legacy section tables
@@ -9340,32 +9364,36 @@
 bioSeq *oSeqList = NULL, *oSeq = NULL;
 struct psl *psl;
 int start;
 enum gfType tt, qt;
 boolean isProt;
 /* In hgBlat's new table mode (blatNewPage), or wherever the modernAlignPage flag is set, show a
  * modern single-page alignment instead of the classic two-frame <frameset>. */
 boolean modern = cartUsualBoolean(cart, "blatNewPage", FALSE)
                  || cfgOptionBooleanDefault("modernAlignPage", FALSE);
 
 char title[1024];
 safef(title, sizeof title, "User Sequence vs Genomic");
 if (modern)
     {
     char pageTitle[256];
-    safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", blatAsmLabel(database));
+    char *asmLabel = blatAsmLabel(database);   // may carry a hub's genomes.txt organism string
+    char *asmLabelHtml = htmlEncode(asmLabel);
+    safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", asmLabelHtml);
     webStartGbNoBanner(cart, database, pageTitle);   // menubar + <main>, no legacy section tables
+    freeMem(asmLabelHtml);
+    freeMem(asmLabel);
     }
 else
     htmlFramesetStart(title);
 
 start = cartInt(cart, "o");
 parseSs(fileNames, &pslName, &faName, &qName);
 if (modern && (!fileExists(pslName) || !fileExists(faName)))
     {   /* the search's trash files have been cleaned up: a friendly note, not a raw file error */
     printf("<p>This BLAT alignment is no longer available. The search results it came from have "
            "expired. Please run a new <a href=\"hgBlat\">BLAT search</a>.</p>\n");
     webEndGb();
     exit(0);
     }
 pslxFileOpen(pslName, &qt, &tt, &lf);
 isProt = (qt == gftProt);
@@ -9377,67 +9405,71 @@
     }
 lineFileClose(&lf);
 if (psl == NULL)
     errAbort("Couldn't find alignment at %s:%d", seqName, start);
 oSeqList = faReadAllSeq(faName, !isProt);
 for (oSeq = oSeqList; oSeq != NULL; oSeq = oSeq->next)
     {
     if (sameString(oSeq->name, qName))
 	break;
     }
 if (oSeq == NULL)  errAbort("%s is in %s but not in %s. Internal error.", qName, pslName, faName);
 if (modern)
     {
     showSomeAlignmentModern(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0, TRUE);   // hgBlat context
     webEndGb();
+    cartCheckout(&cart);   // exit(0) below skips main's checkout, so save the cart here or lose it
     exit(0);   // we drew the whole page; skip the framework's table-closing cartHtmlEnd
     }
 else
     showSomeAlignment(psl, oSeq, qt, 0, oSeq->size, NULL, 0, 0);        // classic frameset; exits itself
 }
 
 void htcBlatAlign(char *qName)
 /* Durable base-by-base alignment for a shared BLAT link (g=htcBlatAlign): rebuild one alignment from
  * the saved session's durable bigPsl custom track (blatLastBigBed) instead of the ephemeral trash
  * .pslx/.fa the fresh-search htcUserAli path reads.  seqName and o identify the hit; the query
  * sequence comes from the bigPsl record itself, so no stored trash sequence is needed.  This backs
  * the "Share a link" button on the modern alignment page. */
 {
 char pageTitle[256];
 safef(pageTitle, sizeof pageTitle, "BLAT Base Alignment: %s", database);
 webStartGbNoBanner(cart, database, pageTitle);   // menubar + <main>, no legacy section tables
 char *bbFile = blatFindPinnedBigPsl(cart);
 if (bbFile == NULL || !fileExists(bbFile))
     {
     printf("<p>This shared BLAT alignment is no longer available. The custom track that stored it "
            "has expired or been removed. Please run a new <a href=\"hgBlat\">BLAT search</a>.</p>\n");
     webEndGb();
+    cartCheckout(&cart);   // this early return also loaded a session; save it before exit(0)
     exit(0);
     }
 int start = cartInt(cart, "o");
 char *seq = NULL;
 struct psl *psl = pslFromBigPslFileMatch(bbFile, seqName, start, qName, &seq, NULL);
 if (psl == NULL || seq == NULL)
     {
     printf("<p>This alignment was not found in the shared BLAT results.</p>\n");
     webEndGb();
+    cartCheckout(&cart);   // this early return also loaded a session; save it before exit(0)
     exit(0);
     }
 enum gfType qType = pslIsProtein(psl) ? gftProt : gftDna;
 struct dnaSeq *oSeq = newDnaSeq(cloneString(seq), strlen(seq), qName);
 showSomeAlignmentModern(psl, oSeq, qType, 0, oSeq->size, NULL, 0, 0, TRUE);   // hgBlat shared-link context
 webEndGb();
+cartCheckout(&cart);   // exit(0) below skips main's checkout, so save the cart here or lose it
 exit(0);   // we drew the whole page; skip the framework's table-closing cartHtmlEnd
 }
 
 void htcProteinAli(char *readName, char *table)
 /* Show protein to translated dna alignment for accession. */
 {
 struct psl *psl;
 int start;
 enum gfType qt = gftProt;
 struct sqlResult *sr;
 struct sqlConnection *conn = hAllocConn(database);
 struct dnaSeq *seq = NULL;
 char query[256], **row;
 char fullTable[HDB_MAX_TABLE_STRING];
 boolean hasBin;
@@ -29078,30 +29110,33 @@
  * This is used to show alignments by hashing the organism associated with the
  * track to the database name where the chromInfo is stored. For example, the
  * mousBlat track in the human browser would hash to the mm2 database. */
 {
 orgDbHash = hashNew(8);
 }
 
 void cartDoMiddle(struct cart *theCart)
 /* Save cart and do main middle handler. */
 {
 initOrgDbHash();
 cart = theCart;
 doMiddle();
 }
 
-char *excludeVars[] = {"Submit", "submit", "g", "i", "aliTable", "addp", "pred", "quickLiftCcds", NULL};
+// "u"/"s" are the shared BLAT link's session selectors (loadBlatShareSessionIfAny); exclude them so
+// they are not left in the reader's cart and written into any session they later save.
+char *excludeVars[] = {"Submit", "submit", "g", "i", "aliTable", "addp", "pred", "quickLiftCcds",
+                       "u", "s", NULL};
 
 int main(int argc, char *argv[])
 {
 long enteredMainTime = clock1000();
 /* 0, 0, == use default 10 second for warning, 20 second for immediate exit */
 issueBotWarning = earlyBotCheck(enteredMainTime, "hgc", delayFraction, 0, 0, "html");
 pushCarefulMemHandler(LIMIT_2or6GB);
 cgiSpoof(&argc,argv);
 cartEmptyShell(cartDoMiddle, hUserCookie(), excludeVars, NULL);
 cgiExitTime("hgc", enteredMainTime);
 return 0;
 }