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; }