09f26ed9a7dcc52b03b4d3e5b2f97c5177cd9334
max
  Mon Sep 21 06:00:40 2026 -0700
Drop the ${hgsid} trackDb variable; add session ids to links in the browser instead

A description page's links can carry the session id without the page itself holding
one.  addHgsidToLinks() in utils.js walks the rendered page and appends hgsid to every
<a href> that stays on this host and points into the same cgi-bin directory: a relative
CGI link gets one, a static .html, a link to another host, a mailto and a plain #anchor
do not, and a link that already names a session is left alone.  hgc and hgTrackUi call
it through a new jsAddHgsidToLinks(), and hgTracks.js calls it on the track description
popup once the ajax content is in.  A link written with a literal $hgsid is rewritten
rather than skipped, so the description pages already deployed in the GenArk hubs work
again.

hVarSubst no longer knows about hgsid: it is out of the trackDb variable list, so
hVarSubstTrackDbHtml is a hub-only pass again and needs no cart, and hVarSubstWithCart
and webIncludeHelpFileSubst, which existed only to resolve it, are gone.  The variable
is taken out of the trackDb README and out of the twenty-odd description pages that
used it.

refs #38380

diff --git src/hg/js/utils.js src/hg/js/utils.js
index f775206a862..157dd006b69 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -1441,30 +1441,80 @@
     hgsid = getURLParam(window.location.href, "hgsid");
     if (hgsid.length > 0)
         return hgsid;
 
     // This may be moved to 1st position as the most likely source
     if (typeof(common) !== 'undefined' && common.hgsid !== undefined && common.hgsid !== null)
         return common.hgsid;
 
     hgsid = normed($("input#hgsid").first());
     if (hgsid)
         return hgsid.value;
 
     return "";
 }
 
+function addHgsidToLinks(root)
+{// add the session id to every link under root that points at one of our own CGIs
+ // A track description page is written by whoever wrote the track or the hub, and is only
+ // lightly sanitized, so the session id cannot be put into it on the server: an <img> in
+ // such a page would then send the id to whatever host the page names. Doing it here means
+ // only an <a href> that stays on this host, in this cgi-bin directory, ever sees it, and
+ // the id is never in what the page's author gets to read back.
+    var hgsid = getHgsid();
+    if (!hgsid)
+        return;
+    // the page doing this is itself a CGI, so its own directory is the cgi-bin directory
+    var cgiDir = window.location.pathname.replace(/[^\/]*$/, '');
+    var links = (root || document).querySelectorAll('a[href]');
+    for (var i = 0; i < links.length; i++) {
+        var href = links[i].getAttribute('href');
+        // an empty href, or one that only jumps within this page, is not a link to a CGI
+        if (!href || href.charAt(0) === '#')
+            continue;
+        var url;
+        try {
+            url = new URL(href, document.baseURI);
+        } catch (e) {
+            continue;     // mailto:, a malformed href, anything we cannot place
+        }
+        if (url.host !== window.location.host || url.pathname.replace(/[^\/]*$/, '') !== cgiDir)
+            continue;
+        // a CGI has no filename extension; a .html or a .png sitting in the same directory
+        // is not one, and neither is a link to the directory itself
+        if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(url.pathname.replace(/^.*\//, '')))
+            continue;
+        // keep the fragment at the end, and leave the rest of the href byte for byte: it may
+        // hold encodings that a round trip through URL would rewrite
+        var hash = '';
+        var hashAt = href.indexOf('#');
+        if (hashAt !== -1) {
+            hash = href.substring(hashAt);
+            href = href.substring(0, hashAt);
+        }
+        // a description page written while the server still filled in a $hgsid variable can
+        // carry the reference itself.  Nothing resolves it now, and hgsid=$hgsid is not a
+        // session id, so drop it and let the real one take its place below.
+        href = href.replace(/([?&])hgsid=(?:\$\{hgsid\}|\$hgsid)(&|$)/i, '$1')
+                   .replace(/[?&]$/, '');
+        if (/[?&]hgsid=/i.test(href))
+            continue;
+        links[i].setAttribute('href', href + (href.indexOf('?') === -1 ? '?' : '&') +
+                              'hgsid=' + encodeURIComponent(hgsid) + hash);
+    }
+}
+
 function undecoratedDb(db)
 // return the db name with any hub_id_ stripped
 {
 var retDb = db;
 if (db.startsWith("hub_")) {
     retDb = db.split('_').slice(2).join('_');
 }
 return retDb;
 }
 
 function getDb()
 {
     var db = normed($("input[name='db']").first());
     if (db)
         return db.value;