81d7cff6cb6665feb772b1339b6298273a3c49be max Thu Aug 6 08:24:38 2026 -0700 hgBlat new results page: table redesign, rename modal, share link, and an XSS fix Iterates on the new hgBlat "table" results page (rendered by hg/js/hgBlat.js from JSON emitted by hgBlat.c) per feedback on #37893: - Columns: "Open in Genome Browser" (position link + new-tab icon), "Show" (base alignment), Query coverage moved before Locus, score-scaled bar in the Score column, Identity as plain %, comma-formatted Span. Locus is plain text, clipped with a CSS ellipsis (full value in title). - Tooltips on every action link and every column header (via the existing convertTitleTagsToMouseovers mechanism). - "Rename BLAT Track": a real modal dialog (replaces the old inline toggle form), reusing the existing hgc buildBigPsl call via a new window.blatRenameCt() helper; it no longer depends on a generic page-global. New cfg fields canRename / trackName / trackDescription drive it. - "Share a link": trash-backed stable URL toggle, with a share-nodes icon. - Security: cgiEncode the query name in the htcUserAli detailsUrl (its sibling already did), and htmlEncode every URL before it goes into an href in hgBlat.js, so a crafted query/sequence name can't break out of the attribute (XSS). - Shared htmlEncode() moved into hg/js/utils.js for reuse instead of a per-file escaper. - hg.conf: blatNewPageBanner (invite banner, default off), blatOldTracks (keep/hide/delete previous BLAT tracks at creation) documented in ex.hg.conf; hgc.c tags BLAT tracks with blatResult=on and clears prior ones per blatOldTracks. refs #37893 diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c index e8ae42ba93d..1f822180985 100644 --- src/hg/hgc/hgc.c +++ src/hg/hgc/hgc.c @@ -9073,94 +9073,110 @@ (ident >= 90) ? "#b45309" : "#b1301f"; /* Offer "Share a link" only when a durable bigPsl custom track backs these results; without it there * is nothing for a shared session to rebuild the alignment from. */ char *shareBb = blatFindPinnedBigPsl(cart); boolean canShare = (shareBb != NULL); freeMem(shareBb); /* Colors imported from the BLAT Redesign (slide 3): grey page, steel-blue section-header bars, navy * links with maroon hover, slate text. The <h2>/<hr> the shared alignment code emits are hidden; its * <h4> section headings become the steel-blue bars. The page chrome is webStartGbNoBanner (a menubar * and <main>, no legacy section tables), so we draw our own gold title bar in plain HTML. */ printf("<style>" "#main-menu-whole{margin-bottom:0}" /* no gap between the menubar and the title bar */ "#mainContent{background:#eef1f4}" /* grey page behind the white alignment panel */ - ".blatTitleBar{background:#e9cf9a; color:#0a2b6b; box-sizing:border-box; display:flex;" + ".blatTitleBar{background:#eaca92; color:#000; box-sizing:border-box; display:flex;" " align-items:center; justify-content:space-between; padding:8px 16px}" /* gold title band */ ".blatTitleBar .blatTtl{font-size:18px; font-weight:700}" ".blatTitleBar .blatBtns{display:flex; gap:8px; align-items:center}" ".blatBtn{padding:4px 12px; font-size:13px; border:1px solid #999; border-radius:3px;" " background:#e6e6e6; text-decoration:none; white-space:nowrap; cursor:pointer}" /* nice_menu.css sets a:link blue (specificity 0,1,1); a.blatBtn:link (0,2,1) beats it */ "a.blatBtn:link, a.blatBtn:visited, a.blatBtn:hover{color:#000; text-decoration:none}" ".blatBtn:hover{background:#d8d8d8}" "#blatAlnBody{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color:#374a5e;" " display:grid; grid-template-columns:220px 1fr;" /* full-height sidebar + content column */ " background:#fff}" /* edge to edge: no margin, no border */ "#blatAlnBody a{color:#0a3a7a}" "#blatAlnBody a:hover{color:#8b1a1a}" "#blatAlnNav{grid-column:1; grid-row:1; background:#f4f7fb; border-right:1px solid #dde3ea}" /* keep the grey column full height, but pin the links so they stay visible while scrolling */ "#blatAlnNavInner{position:sticky; top:0; padding:18px 20px; display:flex; flex-direction:column;" " gap:16px}" "#blatAlnNav a{font-weight:700; text-decoration:none}" /* already obviously links; no underline */ "#blatAlnBlocks{display:flex; flex-direction:column; gap:8px; margin:2px 0 0 14px}" /* block links, indented under side-by-side */ "#blatAlnBlocks a{font-weight:400; font-size:13px}" "#blatAlnContent{grid-column:2; grid-row:1; min-width:0; padding:0 20px 14px}" "#blatAlnContent h2{display:none}" "#blatAlnContent hr{display:none}" - "#blatAlnContent h4{margin:16px -20px 0; padding:8px 20px; background:#4c7093; color:#fff;" + "#blatAlnContent h4{margin:16px -20px 0; padding:8px 20px; background:#4c759c; color:#fff;" " font-size:15px; font-weight:700}" /* -20px: bar spans full content width */ "#blatAlnContent h4:first-child{margin-top:0}" /* Alignment Summary flush at top */ "#blatAlnContent h4 a{color:#fff}" /* undo bootstrap.css (pulled in by webStartGbNoBanner's gbHeader) on the sequence blocks: * it would give <pre> a grey box, a border and word-break that mangles the alignment */ "#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 a * "Share a link" button (when a durable track backs the results) and a "Back to results" button. */ printf("<div class='blatTitleBar'>"); printf("<span class='blatTtl'>BLAT Base Alignment: %s</span>", blatAsmLabel(database)); printf("<span class='blatBtns'>"); printf("<a href='hgBlat?blatReopen=1&hgsid=%s' class='blatBtn'>" "\xe2\x80\xb9 Back to results</a>", 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 */ printf("<div id='blatAlnBody'>\n"); printf("<div id='blatAlnContent'>\n"); printf("<h4>Alignment Summary</h4>\n"); /* comma-format the coordinates and base counts, matching the new Table view (readable at the * hundreds-of-millions scale of genomic coordinates, and the convention elsewhere in the browser) */ char tStartC[32], tEndC[32], matchC[32], qSizeC[32]; sprintLongWithCommas(tStartC, psl->tStart + 1); sprintLongWithCommas(tEndC, psl->tEnd); sprintLongWithCommas(matchC, psl->match + psl->repMatch); sprintLongWithCommas(qSizeC, psl->qSize); -printf("<p><b>%s</b> aligned to <b>%s:%s-%s</b>, " - "<b style='color:%s'>%.1f%% identity</b>, " - "%s of %s bases matched, strand <b>%s</b>.</p>\n", - qName, chrom, tStartC, tEndC, idColor, ident, - matchC, qSizeC, psl->strand); +/* 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); if (isNotEmpty(aliasStr)) printf("<p>Genome sequence %s is also known as: %s.</p>\n", chrom, aliasStr); /* 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; if (qType == gftRna || qType == gftDna) blockCount = showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE); else blockCount = showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName); printf("</div>\n"); /* #blatAlnContent */ /* Sidebar, emitted after the alignment so blockCount is known; CSS grid puts it back in column 1. * The inner div is position:sticky so the links stay in view as the long alignment scrolls. */ printf("<div id='blatAlnNav'><div id='blatAlnNavInner'>\n"); @@ -27384,40 +27400,71 @@ * Bring up the bigPsl detail page with all the alignments. */ { char *trackName = cartString(cart, "trackName"); char *trackDescription = cartString(cart, "trackDescription"); char *pslName, *faName, *qName; parseSs(fileNames, &pslName, &faName, &qName); struct tempName bigBedTn; trashDirDateFile(&bigBedTn, "hgBlat", "bp", ".bb"); char *bigBedFile = bigBedTn.forCgi; makeBigPsl(pslName, faName, database, bigBedFile); char* host = getenv("HTTP_HOST"); boolean isProt = cgiOptionalString("isProt") != NULL; -char *customTextTemplate = "track type=bigPsl indelDoubleInsert=on indelQueryInsert=on pslFile=%s visibility=pack showAll=on htmlUrl=http://%s/goldenPath/help/hgUserPsl.html %s bigDataUrl=%s name=\"%s\" description=\"%s\" colorByStrand=\"0,0,0 0,0,150\" mouseOver=\"${oChromStart}-${oChromEnd} of ${oChromSize} bp, strand ${oStrand}\"\n"; +// blatResult=on tags this as a BLAT results track so previous ones can be found (see blatOldTracks). +char *customTextTemplate = "track type=bigPsl blatResult=on indelDoubleInsert=on indelQueryInsert=on pslFile=%s visibility=pack showAll=on htmlUrl=http://%s/goldenPath/help/hgUserPsl.html %s bigDataUrl=%s name=\"%s\" description=\"%s\" colorByStrand=\"0,0,0 0,0,150\" mouseOver=\"${oChromStart}-${oChromEnd} of ${oChromSize} bp, strand ${oStrand}\"\n"; char *extraForMismatch = "indelPolyA=on showDiffBasesAllScales=. baseColorUseSequence=lfExtra baseColorDefault=diffBases"; if (isProt) extraForMismatch = ""; char buffer[4096]; safef(buffer, sizeof buffer, customTextTemplate, bigBedTn.forCgi, host, extraForMismatch, bigBedTn.forCgi, trackName, trackDescription); struct customTrack *ctList = getCtList(); struct customTrack *newCts = customFactoryParse(database, buffer, FALSE, NULL, NULL); + +/* Optionally clear PREVIOUS BLAT result tracks (those tagged blatResult=on) so the user is not + * confused about which results are current. hg.conf "blatOldTracks": + * keep (default) - do nothing, every search's track stays as-is + * hide - leave earlier BLAT tracks in the session but set them to hide + * delete - remove earlier BLAT tracks from the session (their trash files age out) + * Only BLAT-tagged tracks are touched; the track just made is left alone. */ +char *oldTracks = cfgOptionDefault("blatOldTracks", "keep"); +if (differentString(oldTracks, "keep")) + { + struct customTrack *ct, *next, *keptList = NULL; + for (ct = ctList; ct != NULL; ct = next) + { + next = ct->next; + if (ct->tdb != NULL && sameOk(trackDbSetting(ct->tdb, "blatResult"), "on")) + { + if (sameString(oldTracks, "hide")) + { + cartSetString(cart, ct->tdb->track, "hide"); + slAddHead(&keptList, ct); /* keep it in the session, just hidden */ + } + /* "delete": drop it from the list so customTracksSaveCart writes it out */ + } + else + slAddHead(&keptList, ct); + } + slReverse(&keptList); + ctList = keptList; + } + theCtList = customTrackAddToList(ctList, newCts, NULL, FALSE); customTracksSaveCart(database, cart, theCtList); /* Pin this bigPsl file in the cart so hgBlat's Table view can reopen exactly these results from a * shared session (see doShareReopen in hgBlat.c) - unambiguously, even when the cart holds several * BLAT custom tracks from earlier searches. */ cartSetString(cart, "blatLastBigBed", bigBedFile); cartSetString(cart, "i", "PrintAllSequences"); hgCustom(newCts->tdb->track, NULL); if (sameOk(cartOptionalString(cart, "autoRearr"), "1")) { char snakeVar[256];