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/hgBlat/hgBlat.c src/hg/hgBlat/hgBlat.c index 807d24e78dd..5235a06b1c0 100644 --- src/hg/hgBlat/hgBlat.c +++ src/hg/hgBlat/hgBlat.c @@ -514,35 +514,36 @@ browserHelp, browserUrl, psl->tName, psl->tStart + 1, psl->tEnd, database, pslName, faName, uiState, unhideTrack); printf("<A TITLE='%s' TARGET=_BLANK HREF=\"%s?position=%s:%d-%d&db=%s&ss=%s+%s&%s\">new tab%s</A> ", helpText, browserUrl, psl->tName, psl->tStart + 1, psl->tEnd, database, pslName, faName, unhideTrack, icon); } } printf("<A title='Show query sequence, genome hit and sequence alignment' " "HREF=\"%s?o=%d&g=htcUserAli&i=%s+%s+%s&c=%s&l=%d&r=%d&db=%s&%s\">", hgcUrl, psl->tStart, pslName, cgiEncode(faName), psl->qName, psl->tName, psl->tStart, psl->tEnd, database, uiState); printf("details</A> "); } static char *chromTypeNote(char *tName) -/* Return a short explanation for _alt/_fix/_random/chrUn sequences, or NULL for a normal chrom. */ +/* Return a short explanation for special sequence names (alt/fix/random/hap/unplaced), or NULL for a + * normal chromosome. tName should be the display name the user sees. */ { if (endsWith(tName, "_fix")) return "Assembly fix patch: corrects an error in the reference assembly."; -if (endsWith(tName, "_alt")) +if (endsWith(tName, "_alt") || stringIn("_hap", tName)) return "Alternate haplotype: an alternate sequence for this region."; if (endsWith(tName, "_random")) return "Unlocalized sequence: known chromosome, position not determined."; if (startsWith(tName, "chrUn")) return "Unplaced sequence: chromosome of origin unknown."; return NULL; } static char *blatBrowserUrl(struct psl *psl, char *database, char *browserUrl, char *pslName, char *faName, char *customText, char *uiState, char *unhideTrack, boolean withUiState) /* Return a Genome Browser URL for one BLAT hit. withUiState appends the hgsid; it is included on * the in-tab link but omitted from the new-tab link, matching the classic hyperlink behavior. */ { struct dyString *dy = dyStringNew(256); dyStringPrintf(dy, "%s?position=%s:%d-%d&db=%s", browserUrl, psl->tName, psl->tStart + 1, psl->tEnd, database); @@ -607,91 +608,137 @@ jsonWriteObjectStart(jw, NULL); jsonWriteObjectStart(jw, "config"); /* For assembly/GenArk hubs the internal names carry a "hub_NNN_" prefix; drop it so the Assembly * field reads cleanly (and doesn't show the prefix twice), matching the BLAT Results page title. */ jsonWriteString(jw, "db", trackHubSkipHubName(database)); jsonWriteString(jw, "organism", trackHubSkipHubName(organism)); jsonWriteString(jw, "queryName", pslList->qName); jsonWriteNumber(jw, "querySize", pslList->qSize); jsonWriteNumber(jw, "hitCount", slCount(pslList)); jsonWriteBoolean(jw, "multiQuery", pslListMultiQuery(pslList)); jsonWriteBoolean(jw, "hasLocus", locusConn != NULL); /* Sharing a link only makes sense when a durable bigPsl custom track was made from the results * (autoBigPsl); otherwise there is nothing for the shared session to reopen from. */ jsonWriteBoolean(jw, "canShare", autoBigPsl); +/* Renaming the results custom track needs the bigPsl track + the C blatRenameCt() helper, both only + * present with autoBigPsl. hgBlat.js shows its own "Rename BLAT Track" button + modal in that case, + * pre-filled with the track's current name/description (below) so it needs no page-global. */ +jsonWriteBoolean(jw, "canRename", autoBigPsl); +if (autoBigPsl) + { + char *ctName = NULL, *ctDescription = NULL; + getCustomName(database, cart, pslList, &ctName, &ctDescription); + jsonWriteString(jw, "trackName", ctName); + jsonWriteString(jw, "trackDescription", ctDescription); + } /* The classic "Old BLAT result page" view re-reads the trash .pslx from the current search, so it * is only offered on a fresh search (pslName set), not on a shared-link reopen rebuilt from the * durable custom track (where the trash files may be long gone). */ jsonWriteBoolean(jw, "canOldPage", pslName != NULL); jsonWriteString(jw, "hgsid", cartSessionId(cart)); jsonWriteStringf(jw, "newSearchUrl", "hgBlat?db=%s&%s", database, uiState); +/* Stable, shareable page URL: it reopens straight from the trash .pslx/.fa (no saved session, no + * sessionData - the link works until trash is cleaned). Carry only the random basenames as tokens; + * the reopen reconstructs the trash paths, so no filesystem path or session id is exposed in the URL, + * and the recipient uses their own cart. Present whenever the trash files exist (fresh search or a + * trash reopen), so hgBlat.js can pin it into the address bar with history.replaceState(). */ +if (pslName != NULL && faName != NULL) + { + char *pslId = cloneString(strrchr(pslName, '/') ? strrchr(pslName, '/') + 1 : pslName); + char *faId = cloneString(strrchr(faName, '/') ? strrchr(faName, '/') + 1 : faName); + chopSuffix(pslId); /* drop ".pslx" -> bare token */ + chopSuffix(faId); /* drop ".fa" -> bare token */ + jsonWriteStringf(jw, "shareUrl", "hgBlat?blatNewPage=1&db=%s&blatPslId=%s&blatFaId=%s", + database, pslId, faId); + freeMem(pslId); + freeMem(faId); + } char *posStr = cartOptionalString(cart, "position"); if (posStr != NULL) { jsonWriteString(jw, "backUrl", browserUrl); jsonWriteString(jw, "backPos", posStr); } struct dyString *va = dyStringNew(128); dyStringPrintf(va, "%s?db=%s", browserUrl, database); if (customText) dyStringPrintf(va, "&hgt.customText=%s", customText); else if (!autoBigPsl && pslName != NULL) dyStringPrintf(va, "&ss=%s+%s", pslName, faName); dyStringPrintf(va, "&%s%s", uiState, unhideTrack); jsonWriteString(jw, "viewAllUrl", va->string); dyStringFree(&va); -jsonWriteStringf(jw, "geneUrlBase", "%s?db=%s&%s&position=", browserUrl, database, uiState); +/* The query sequence(s) for the "Show Query Sequence" button. Only available on a fresh search, + * where the uploaded FASTA is still in trash (faName); on a shared-link reopen faName is NULL and + * the button is omitted client-side. */ +if (faName != NULL && fileExists(faName)) + { + struct dnaSeq *qSeqList = faReadAllMixed(faName), *qSeq; + if (qSeqList != NULL) + { + jsonWriteListStart(jw, "querySeqs"); + for (qSeq = qSeqList; qSeq != NULL; qSeq = qSeq->next) + { + jsonWriteObjectStart(jw, NULL); + jsonWriteString(jw, "name", qSeq->name); + jsonWriteString(jw, "seq", qSeq->dna); + jsonWriteObjectEnd(jw); + } + jsonWriteListEnd(jw); + } + dnaSeqFreeList(&qSeqList); + } jsonWriteObjectEnd(jw); // config jsonWriteListStart(jw, "hits"); int rank = 0; for (psl = pslList; psl != NULL; psl = psl->next) { ++rank; double ident = 100.0 - pslCalcMilliBad(psl, TRUE) * 0.1; char *displayChromName = chromAliasGetDisplayChrom(database, cart, psl->tName); char *inTabUrl = blatBrowserUrl(psl, database, browserUrl, pslName, faName, customText, uiState, unhideTrack, TRUE); char *newTabUrl = blatBrowserUrl(psl, database, browserUrl, pslName, faName, customText, uiState, unhideTrack, FALSE); jsonWriteObjectStart(jw, NULL); jsonWriteNumber(jw, "rank", rank); jsonWriteString(jw, "qName", psl->qName); jsonWriteNumber(jw, "score", pslScore(psl)); jsonWriteDouble(jw, "identity", ident); jsonWriteString(jw, "chrom", displayChromName); - char *note = chromTypeNote(psl->tName); + char *note = chromTypeNote(displayChromName); /* note must match the NAME the user sees */ if (note != NULL) jsonWriteString(jw, "chromNote", note); jsonWriteString(jw, "strand", psl->strand); jsonWriteNumber(jw, "tStart", psl->tStart + 1); jsonWriteNumber(jw, "tEnd", psl->tEnd); jsonWriteNumber(jw, "span", psl->tEnd - psl->tStart); jsonWriteNumber(jw, "qStart", psl->qStart + 1); jsonWriteNumber(jw, "qEnd", psl->qEnd); jsonWriteNumber(jw, "qSize", psl->qSize); jsonWriteNumber(jw, "matches", psl->match + psl->repMatch); jsonWriteNumber(jw, "misMatch", psl->misMatch); jsonWriteNumber(jw, "gaps", psl->qNumInsert + psl->tNumInsert); jsonWriteNumber(jw, "blocks", psl->blockCount); jsonWriteString(jw, "browserUrl", inTabUrl); jsonWriteString(jw, "newTabUrl", newTabUrl); if (pslName != NULL) jsonWriteStringf(jw, "detailsUrl", "%s?o=%d&g=htcUserAli&i=%s+%s+%s&c=%s&l=%d&r=%d&db=%s&%s", - hgcUrl, psl->tStart, pslName, cgiEncode(faName), psl->qName, psl->tName, + hgcUrl, psl->tStart, pslName, cgiEncode(faName), cgiEncode(psl->qName), psl->tName, psl->tStart, psl->tEnd, database, uiState); else /* Shared-link reopen: there is no trash .pslx, but the durable bigPsl custom track (now in * this cart) lets hgc's htcBlatAlign rebuild the base alignment from the stored query seq. * chrom/start/qName select the hit; db and the browser window come from the loaded cart. */ jsonWriteStringf(jw, "detailsUrl", "%s?g=htcBlatAlign&db=%s&c=%s&o=%d&i=%s&%s", hgcUrl, database, psl->tName, psl->tStart, cgiEncode(psl->qName), uiState); if (locusConn) { struct sqlResult *sr = hRangeQuery(locusConn, "locusName", psl->tName, psl->tStart, psl->tEnd, NULL, 0); char **row = sqlNextRow(sr); if (row != NULL) { char *raw = row[4]; char *full = subTextString(subList, raw); @@ -724,46 +771,49 @@ freeMem(inTabUrl); freeMem(newTabUrl); } jsonWriteListEnd(jw); // hits jsonWriteObjectEnd(jw); // root printf("<div id='blatResults'></div>\n"); jsInlineF("var hgBlatData = %s;\n", jw->dy->string); jsonWriteFree(&jw); } static void printNewDisplayBanner(char *uiState) /* On the classic hyperlink results page, offer a one-click switch to the modern Table display. * The link sets the blatNewPage cart variable (so the choice sticks for future searches) and * reopens the current results (blatReopen) in the new format. - * The banner is on by default but can be turned off in hg.conf (blatNewPageBanner=off) to stop - * advertising the new page - without releasing new CGIs - while the display itself stays available - * to users who already opted in or use a direct link. */ + * The banner is OFF by default while the new page is still being tested; set blatNewPageBanner=on + * in hg.conf to advertise the new page - without releasing new CGIs. The new display itself stays + * reachable by users who already opted in or use a direct blatNewPage=1 link. */ { -if (!cfgOptionBooleanDefault("blatNewPageBanner", TRUE)) +if (!cfgOptionBooleanDefault("blatNewPageBanner", FALSE)) return; -printf("<div style=\"display:flex;align-items:center;gap:16px;max-width:900px;" - "margin:0 0 20px;padding:14px 18px;border:1px solid #cfe0f5;border-radius:6px;" - "background:#f0f6ff\">" - "<span style=\"flex:1;font-size:15px;color:#1f2937;line-height:1.5\">" - "There is a new BLAT results page, with a sortable and filterable table of hits, " - "gene loci and query coverage.</span>" - "<a href=\"hgBlat?blatNewPage=1&blatReopen=1&%s\" " - "style=\"white-space:nowrap;padding:9px 18px;border-radius:4px;background:#2c5aa0;" - "color:#ffffff;text-decoration:none;font-size:14px;font-weight:600\">" - "Try the new page</a></div>\n", uiState); +/* Reuse the new page's ".blatBanner" look (house style: wheat box, square corners, navy links). + * The classic page doesn't load hgBlat.js's injected CSS, so define the same class here. Keep these + * values in sync with the .blatBanner rule in hgBlat.js. */ +printf("<style>" + ".blatBanner{background:#fbf3e2;border:1px solid #d9bd82;padding:10px 14px;margin:12px 0 20px;" + "font-size:14px;color:#1e2833}" + ".blatBanner a{color:#003a72}" + ".blatBanner a:hover{color:#8b1a1a}" + "</style>" + "<div class=\"blatBanner\">" + "We are testing a BLAT results page, with a sortable and filterable table of hits, " + "gene loci and query coverage. " + "<a href=\"hgBlat?blatNewPage=1&blatReopen=1&%s\">Try the new page</a>.</div>\n", uiState); } void showAliPlaces(char *pslName, char *faName, char *customText, char *database, enum gfType qType, enum gfType tType, char *organism, boolean feelingLucky) /* Show all the places that align. */ { boolean useBigPsl = cfgOptionBooleanDefault("useBlatBigPsl", TRUE); struct lineFile *lf = pslFileOpen(pslName); struct psl *pslList = NULL, *psl; char *browserUrl = hgTracksName(); char *hgcUrl = hgcName(); char uiState[64]; char *vis; char unhideTrack[64]; @@ -886,130 +936,128 @@ "var luckyLocation = '';\n" ); if(feelingLucky) { /* If we found something jump browser to there. */ if(slCount(pslList) > 0) printLuckyRedirect(browserUrl, pslList, database, pslName, faName, uiState, unhideTrack); } // RENAME BLAT CT FORM // new re-submit code with new trackname and decription if (!feelingLucky) { printf("<div id=renameFormItem style='display: none'>\n"); printf("<FORM ACTION=>\n"); - printf("<INPUT TYPE=SUBMIT NAME=Submit id='showRenameForm' VALUE=\"Rename Custom Track\">\n"); + printf("<INPUT TYPE=SUBMIT NAME=Submit id='showRenameForm' VALUE=\"Rename BLAT Track\" " + "title=\"Give the BLAT results custom track a name and description of your choosing\">\n"); printf("</FORM>\n"); printf("</div>\n"); printf("<div id=renameForm style='display: none'>\n"); char *hgcUrl = hgcName(); printf( "<DIV STYLE=\"display:block;\"><FORM ACTION=\"%s\" METHOD=\"%s\" NAME=\"customTrackForm\">\n", hgcUrl,cartUsualString(cart, "formMethod", "POST")); printf("<TABLE><TR><TD>Custom track name: "); cgiMakeTextVar( "trackName", "FAKETRACKNAME", 30); printf("</TD></TR>"); printf("<TR><TD> Custom track description: "); cgiMakeTextVar( "trackDescription", "FAKETRACKDESCRIPTION",50); // track description or longLabel printf("</TD></TR>"); // remove the current blat ct so we can create a new one. cgiMakeHiddenVar(CT_DO_REMOVE_VAR, "Remove Custom track"); cgiMakeHiddenVar(CT_SELECTED_TABLE_VAR, "FAKETRACKNAME"); printf("<TR><TD><INPUT TYPE=SUBMIT NAME=Submit id=submitTrackNameDescr VALUE=\"Submit\">\n"); puts("</TD></TR>"); printf("</TABLE></FORM></DIV>"); printf("</div>\n"); } - if (!feelingLucky) - { - // REMOVE CT BUTTON FORM. - printf("<div id=deleteCtForm style='display: none'>\n"); - printf("<FORM ACTION=\"%s?hgsid=%s&db=%s\" NAME=\"MAIN_FORM\" METHOD=%s>\n\n", - hgTracksName(), cartSessionId(cart), database, cartUsualString(cart, "formMethod", "POST")); - cartSaveSession(cart); - cgiMakeButton(CT_DO_REMOVE_VAR, "Delete Custom Track"); - cgiMakeHiddenVar(CT_SELECTED_TABLE_VAR, "FAKETRACKNAME"); - printf("</FORM>\n"); - printf("</div>\n"); - } + /* The standalone "Delete Custom Track" button was removed from the new table page: users had + * no clear reason to delete the track they just made. (The rename flow still removes/replaces + * the old track internally via CT_DO_REMOVE_VAR.) */ jsInlineF( "var ct_blat = '';\n" "\n" "function buildBigPslCtSuccess (content, status)\n" "{ // Finishes the successful creation of blat ct bigPsl. Called by ajax return.\n" " // saves the ct name so it can be used later for rename or delete.\n" "\n" "var matchWord = '&table=';\n" "var ct_blatPos = content.indexOf(matchWord) + matchWord.length;\n" "\n" "if (ct_blatPos >= 0)\n" " {\n" " var ct_blatPosEnd = content.indexOf('\"', ct_blatPos);\n" " ct_blat = content.slice(ct_blatPos, ct_blatPosEnd);\n" " if (luckyLocation == '')\n" " {\n" - " $('input[name=\""CT_SELECTED_TABLE_VAR"\"]')[0].value = ct_blat;\n" - " $('input[name=\""CT_SELECTED_TABLE_VAR"\"]')[1].value = ct_blat;\n" + /* set the ct name on whatever CT_SELECTED_TABLE_VAR inputs are present (the rename form has + * one; the old delete form that had a second was removed), so no [1] undefined error */ + " $('input[name=\""CT_SELECTED_TABLE_VAR"\"]').each(function(){ this.value = ct_blat; });\n" " }\n" " }\n" "}\n" "\n" "function buildBigPslCt (url, trackName, trackDescription)\n" "{ // call hgc to buildBigPsl from blat result.\n" "\n" "var cgiVars = 'trackName='+encodeURIComponent(trackName)+'&trackDescription='+encodeURIComponent(trackDescription);\n" "if (!ct_blat !== '')\n" " {\n" " cgiVars += '&"CT_DO_REMOVE_VAR"='+encodeURIComponent('Remove Custom Track');\n" " cgiVars += '&"CT_SELECTED_TABLE_VAR"='+encodeURIComponent(ct_blat);\n" " }\n" "\n" "$.ajax({\n" " type: 'GET',\n" " url: url,\n" " data: cgiVars,\n" " dataType: 'html',\n" " trueSuccess: buildBigPslCtSuccess,\n" " success: catchErrorOrDispatch,\n" " error: errorHandler,\n" " cache: false,\n" " async: false\n" " });\n" "}\n" "\n" "var url='%s';\n" "var trackName='%s';\n" "var trackDescription='%s';\n" + /* Exposed for hgBlat.js's rename modal: rebuild the custom track under a new name/description, + * reusing the same buildBigPslCt() call and closed-over url, so the client needs no globals. */ + "window.blatRenameCt = function(name, description) {\n" + " trackName = name; trackDescription = description;\n" + " buildBigPslCt(url, name, description);\n" + "};\n" "$(document).ready(function() {\n" "\n" "buildBigPslCt(url, trackName, trackDescription);\n" "if (luckyLocation !== '')\n" " {\n" " location.replace(luckyLocation);\n" " }\n" "else\n" " {\n" " $('#renameFormItem')[0].style.display = 'block';\n" - " $('#deleteCtForm')[0].style.display = 'block';\n" " }\n" "});\n", url->string, trackName, trackDescription); // RENAME CT JS CODE if (!feelingLucky) jsInline("$('#showRenameForm').click(function(){\n" " $('#renameForm')[0].style.display = 'block';\n" " $('#renameFormItem')[0].style.display = 'none';\n" " $('#showRenameForm')[0].style.display = 'none';\n" " $('input[name=\"trackName\"]')[0].value = trackName;\n" " $('input[name=\"trackDescription\"]')[0].value = trackDescription;\n" "return false;\n" "});\n"); @@ -2216,30 +2264,35 @@ gfAlignStrand(conn, serve->nibDir, seq, TRUE, minMatchShown, tFileCache, gvo); } } gfOutputQuery(gvo, f); ++seqNumber; } carefulClose(&f); if (!allGenomes) { /* Remember the trash result files so the Table view's "Old BLAT result page" link can * re-render the classic hyperlink view from them within this session without re-running BLAT * (see doOldPageReopen). These are just short paths; the query sequence is not stored. */ cartSetString(cart, "blatPslFile", pslTn.forCgi); cartSetString(cart, "blatFaFile", faTn.forCgi); + /* Remember the db/organism the search actually ran against, so a later blatReopen renders with + * the right assembly even if the cart's current db has since drifted (e.g. the user searched a + * different assembly in between). The trash PSLs carry this assembly's chrom names. */ + cartSetString(cart, "blatDb", serve->db); + cartSetString(cart, "blatOrganism", organism); showAliPlaces(pslTn.forCgi, faTn.forCgi, NULL, serve->db, qType, tType, organism, feelingLucky); } if ((!feelingLucky && !allGenomes) || (autoBigPsl && feelingLucky)) cartWebEnd(); gfFileCacheFree(&tFileCache); } void askForSeq(char *organism, char *db) /* Put up a little form that asks for sequence. * Call self.... */ { /* ignore struct serverTable* return, but can error out if not found */ @@ -2585,33 +2638,39 @@ if (!gH->isProt) { printf("%d\t", gfR->qFrame); } } printf("\n"); } } printf("\n"); } printf("</PRE>\n"); } +/* LEGACY (see /hive/groups/browser/redmineNotes/37893/): new "Share a link" links no longer save a + * session - they carry the trash .pslx/.fa basenames and reopen via doTrashShareReopen (no session, + * no sessionData; they expire when trash is cleaned). This ?u=&s= session path is kept only so any + * old session-based links still open. A separate longer-lived (~6-12 month) auto-expiring anonymous + * trash tier, for links that should outlive normal trash, is still wanted later (see the + * edit-kent-code skill note "Trash lifetime & durable anonymous storage"). */ static void doShareReopen(char *database, char *organism) -/* Rebuild the Table view for a shared link (?u=&s=) from the durable bigPsl custom track that was - * saved with the session, without re-running BLAT and without any stored query sequence. The +/* Rebuild the Table view for an OLD shared link (?u=&s=) from the durable bigPsl custom track that + * was saved with the session, without re-running BLAT and without any stored query sequence. The * custom track (and its bigBed file) is kept alive by refreshNamedSessionCustomTracks for as long * as the shared session exists, so this is durable. */ { cartWebStart(cart, database, "%s (%s) BLAT Results", trackHubSkipHubName(organism), trackHubSkipHubName(database)); char *bbFile = blatFindPinnedBigPsl(cart); if (bbFile == NULL || !fileExists(bbFile)) { printf("<p>These shared BLAT results are no longer available. The custom track that " "stored them has expired or been removed. Please run a new " "<a href=\"hgBlat\">BLAT search</a>.</p>\n"); cartWebEnd(); return; } struct psl *pslList = pslListFromBigPslFile(bbFile); @@ -2631,40 +2690,90 @@ printBlatResultsApp(pslList, database, organism, hgTracksName(), hgcName(), NULL, NULL, NULL, uiState, "", locusConn, subList); cartWebEnd(); } static void doReopenResults(char *database, char *organism) /* Re-render the last search's results for the current session from the trash result files saved * with it (see blatPslFile/blatFaFile), without re-running BLAT. showAliPlaces picks the classic * or new-table format from the blatNewPage cart variable, so this backs both the classic page's * "Try the new display" banner and the table's "Old BLAT result page" link. Those trash files are * only guaranteed for the current session, so if they have been cleaned up, say so rather than * showing a broken page. */ { char *pslFile = cartOptionalString(cart, "blatPslFile"); char *faFile = cartOptionalString(cart, "blatFaFile"); +/* Use the db/organism the saved search ran against, not the cart's current db (which may have + * drifted): the trash PSLs carry that assembly's chrom names, so rendering under any other db + * gives broken position and "View alignment" links (hgc "bad input variables"). */ +char *savedDb = cartOptionalString(cart, "blatDb"); +if (isNotEmpty(savedDb)) + { + database = savedDb; + char *savedOrg = cartOptionalString(cart, "blatOrganism"); + if (isNotEmpty(savedOrg)) + organism = savedOrg; + } cartWebStart(cart, database, "%s (%s) BLAT Results", trackHubSkipHubName(organism), trackHubSkipHubName(database)); if (pslFile == NULL || faFile == NULL || !fileExists(pslFile)) printf("<p>These BLAT results are no longer available. Please run a new " "<a href=\"hgBlat\">BLAT search</a>.</p>\n"); else showAliPlaces(pslFile, faFile, NULL, database, gftDna, gftDna, organism, FALSE); cartWebEnd(); } +static boolean blatSafeTrashId(char *s) +/* TRUE only if s is a bare trash-file basename token (letters, digits, underscore). Because it can + * contain no '/', '.' or '..', a path built from it cannot escape the trash directory. */ +{ +if (isEmpty(s)) + return FALSE; +char *p; +for (p = s; *p != '\0'; ++p) + if (!isalnum((unsigned char)*p) && *p != '_') + return FALSE; +return TRUE; +} + +static void doTrashShareReopen(char *database, char *organism, char *pslId, char *faId) +/* Reopen a shared BLAT results link that carries the trash .pslx/.fa basenames (blatPslId/blatFaId in + * the URL) - no saved session, no sessionData; the link works until trash is cleaned. Reconstruct + * the trash paths from the validated tokens, point the cart's blatPslFile/blatFaFile at them, and + * hand off to doReopenResults (which rebuilds the Table view and handles cleaned-up trash gracefully). + * The recipient uses their own cart, so pin the shared results' db here (their cart may carry a stale + * blatDb from an earlier search of their own). */ +{ +cartSetString(cart, "blatNewPage", "1"); /* a shared table link always shows the table */ +if (!blatSafeTrashId(pslId) || !blatSafeTrashId(faId)) + { + cartWebStart(cart, database, "BLAT Results"); + printf("<p>This shared BLAT link is not valid.</p>\n"); + cartWebEnd(); + return; + } +char pslPath[1024], faPath[1024]; +safef(pslPath, sizeof pslPath, "%s/hgSs/%s.pslx", trashDir(), pslId); +safef(faPath, sizeof faPath, "%s/hgSs/%s.fa", trashDir(), faId); +cartSetString(cart, "blatPslFile", pslPath); +cartSetString(cart, "blatFaFile", faPath); +cartSetString(cart, "blatDb", database); +cartSetString(cart, "blatOrganism", organism); +doReopenResults(database, organism); +} + void doMiddle(struct cart *theCart) /* Write header and body of html page. */ { char *userSeq; char *db, *organism; boolean clearUserSeq = cgiBoolean("Clear"); allGenomes = cgiVarExists("allGenomes"); cart = theCart; dnaUtilOpen(); /* The former "table" value of the output dropdown is now the blatNewPage toggle; migrate any stale * cart value so the dropdown always shows a valid option. */ if (sameOk(cartOptionalString(cart, "output"), "table")) @@ -2691,30 +2800,37 @@ orgChange = sameOk(cgiOptionalString("changeInfo"),"orgChange"); if (orgChange) cgiVarSet("db", hDefaultDbForGenome(cgiOptionalString("org"))); getDbAndGenome(cart, &db, &organism, oldVars); chromAliasSetup(db); /* A shared "?u=&s=" link rebuilds the Table view from the session's durable custom track; it never * re-runs BLAT, so short-circuit the normal query-driven flow here (before findClosestServer, which * is only needed for an actual search). */ if (cgiVarExists("s")) { doShareReopen(db, organism); return; } +/* Stable shareable-link reopen (blatPslId/blatFaId): rebuild the Table straight from the trash + * .pslx/.fa named in the URL - no session, no sessionData. See doTrashShareReopen. */ +if (cgiOptionalString("blatPslId") != NULL && cgiOptionalString("blatFaId") != NULL) + { + doTrashShareReopen(db, organism, cgiString("blatPslId"), cgiString("blatFaId")); + return; + } /* The classic page's "Try the new display" banner and the Table view's "Old BLAT result page" link * both flip the blatNewPage preference and re-render this session's saved results in the other * format, without re-running BLAT. */ if (cgiVarExists("blatReopen")) { doReopenResults(db, organism); return; } char *oldDb = cloneString(db); // n.b. this changes to default db if db doesn't have BLAT findClosestServer(&db, &organism); allResults = cartUsualBoolean(cart, "allResults", allResults);