c0e8fa6df3a0bd406c4188d49ee00f20aef203e5
max
  Mon Sep 7 12:07:18 2026 -0700
Substitute trackDb variables in hub track description pages

A hub's description page comes straight off the hub's web server and has
never been through variable substitution, so a $db or $parentTrack in it
reached the reader as literal text. Native trackDb pages are fine, since
hgTrackDb substitutes them when it loads trackDb, but there was no
equivalent step for a hub.

hgc's getTrackHtml and hgTrackUi's trackUi both call hVarSubstTrackDbHtml
on a hub track's html.  Only a short list of variables is recognized there and nothing is an
error, because a hub page written before this existed can easily contain
a dollar sign inside a shell example, and silently rewriting that would
be worse than not substituting at all.

Adds $parentTrack, the name of the container a track sits in, which is
what a subtrack description page needs to link back to its superTrack or
composite. Views are skipped, since a view has no page of its own, and
the hub_<id>_ prefix is kept so the name works as hgTrackUi's g=
parameter. Documents $track, $parentTrack and $hgsid in trackDb/README.

refs #37599

diff --git src/hg/lib/hVarSubst.c src/hg/lib/hVarSubst.c
index 07a96decb4c..d471025f201 100644
--- src/hg/lib/hVarSubst.c
+++ src/hg/lib/hVarSubst.c
@@ -1,27 +1,28 @@
 /* Handle variable substitutions in strings from trackDb and other
  * labels. See trackDb/README for descriptions of values that can be
  * substitute.  This code needs to do a special handle to remain compatibility
  * with behavior of the old substitution mechanism. */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 #include "common.h"
 #include "trackDb.h"
 #include "hdb.h"
 #include "hui.h"
 #include "sqlNum.h"
+#include "hubConnect.h"
 #include "hVarSubst.h"
 
 static boolean isVarEnd(boolean inBraces, char c)
 /** does the character end a variable reference. */
 {
 if (inBraces)
     return (c == '}');
 else
     return !((c == '_') || isalnum(c));
 }
 
 static char *parseVarName(char *desc, char *varStart, char *varName, int varNameSize)
 /* parse substitution variable name out of a string, returning next position
  * after name. */
 {
@@ -33,30 +34,54 @@
     p++;
 int nameIdx = 0;
 while ((nameIdx < varNameSize-1) && !isVarEnd(inBraces, *p))
     varName[nameIdx++] = *p++;
 if (nameIdx == 0)
     errAbort("empty variable name in %s varStart='%s' varName='%s'", desc, varStart, varName);
 if (nameIdx == varNameSize)
     errAbort("variable name in desc %s exceeds maximum length of %d, starting with: \"%.*s\"",
              desc, varNameSize-1, varNameSize-1, varName);
 varName[nameIdx] = '\0';
 if (inBraces)
     p++;
 return p;
 }
 
+static char *parseVarNameMaybe(char *varStart, char *varName, int varNameSize)
+/* Like parseVarName, but return NULL instead of aborting when what follows the `$' is not
+ * a well formed variable reference.  Used in hubHtml mode, where a stray dollar sign in a
+ * description page has to survive untouched. */
+{
+char *p = varStart+1;
+boolean inBraces = (*p == '{');
+if (inBraces)
+    p++;
+int nameIdx = 0;
+while ((nameIdx < varNameSize-1) && (*p != '\0') && !isVarEnd(inBraces, *p))
+    varName[nameIdx++] = *p++;
+if ((nameIdx == 0) || (nameIdx == varNameSize-1))
+    return NULL;
+varName[nameIdx] = '\0';
+if (inBraces)
+    {
+    if (*p != '}')
+        return NULL;
+    p++;
+    }
+return p;
+}
+
 static char *lookupTrackDbSubVar(char *desc, struct trackDb *tdb, char *settingName, char *varName)
 /* get the specified track setting to substitute or die trying; more useful
  * message than trackDbRequiredSetting when doing substitution */
 {
 char *val = trackDbSettingClosestToHome(tdb, settingName);
 if (val == NULL)
    errAbort("trackDb (%s) setting \"%s\" not found for variable substitution of \"$%s\" in %s",
             tdb->track, settingName, varName, desc);
 return val;
 }
 
 static char *lookupOtherDb(char *desc, struct trackDb *tdb, char *varName)
 /* look up the otherDb variable, which is needed for substituting varName */
 {
 return lookupTrackDbSubVar(desc, tdb, "otherDb", varName);
@@ -215,129 +240,203 @@
     dyStringAppend(dest, org);
     freeMem(org);
     }
 else if (sameString(varBase, "date"))
     {
     char *date = valOrDb(hFreezeDateOpt(database), database);
     dyStringAppend(dest, date);
     freeMem(date);
     }
 else if (sameString(varBase, "db"))
     dyStringAppend(dest, database);
 else if (sameString(varBase, "hgsid") && cart != NULL)
     dyStringAppend(dest, cartSessionId(cart));
 }
 
+static char *parentTrackName(struct trackDb *tdb)
+/* Name of the container holding tdb, in the form hgTrackUi's g= parameter needs: with the
+ * hub_<id>_ prefix when this is a hub track, since that is what trackHubAddNamePrefix put
+ * into tdb->track.  Views are skipped, a view has no description page of its own.  Returns
+ * the track's own name when it is not in a container. */
+{
+struct trackDb *parent = tdb->parent;
+char *viewName = NULL;
+while ((parent != NULL) && tdbIsView(parent, &viewName))
+    parent = parent->parent;
+return (parent != NULL) ? parent->track : tdb->track;
+}
+
+/* The variables a hub's description page may use.  Deliberately a short explicit list and
+ * not every trackDb setting the way native trackDb allows: a hub page written before this
+ * substitution existed can easily contain something like "$track" inside a shell example,
+ * and silently rewriting that would be worse than not substituting at all. */
+static char *hubHtmlVars[] = {"db", "hgsid", "track", "parentTrack",
+                              "organism", "Organism", "ORGANISM", "date", "downloadsServer"};
+
+static boolean isHubHtmlVar(struct cart *cart, struct trackDb *tdb, char *varName)
+/* Is varName one of the variables a hub description page may use, and can this call
+ * resolve it?  Asked only in hubHtml mode, to tell a variable reference from a dollar sign
+ * that happens to be followed by a word. */
+{
+if (tdb == NULL)
+    return FALSE;
+if (sameString(varName, "hgsid") && (cart == NULL))
+    return FALSE;
+int i;
+for (i = 0;  i < ArraySize(hubHtmlVars);  i++)
+    if (sameString(varName, hubHtmlVars[i]))
+        return TRUE;
+return FALSE;
+}
+
 static void substTrackDbVar(char *desc, struct trackDb *tdb, char *database,
                             char *varName, struct dyString *dest)
 /* substitute a variable value obtained from trackDb */
 {
 if (sameString(varName, "matrix"))
     substMatrixHtml(tdb, dest);
 else if (sameString(varName, "chainLinearGap"))
     substLinearGap(tdb, dest);
 else if (sameString(varName, "downloadsServer"))
     dyStringAppend(dest, hDownloadsServer());
 else if (sameString(varName, "track"))
     dyStringAppend(dest, tdb->track);
+else if (sameString(varName, "parentTrack"))
+    dyStringAppend(dest, parentTrackName(tdb));
 else
     dyStringAppend(dest, lookupTrackDbSubVar(desc, tdb, varName, varName));
 }
 
 static void substVar(char *desc, struct cart *cart, struct trackDb *tdb, char *database,
                      char *varName, struct dyString *dest)
 /* look up varName and insert value in output string.  Error if variable
  * can't be found */
 {
 if (isDatabaseVar(varName))
     substDatabaseVar(database, cart, varName, dest);
 else if (tdb == NULL)
     errAbort("invalid variable \"%s\" to substitute in %s",
              varName, desc);
 else if (startsWith("o_", varName) && isDatabaseVar(varName+2))
     substDatabaseVar(lookupOtherDb(desc, tdb, varName), cart, varName+2, dest);
 else
     substTrackDbVar(desc, tdb, database, varName, dest);
 }
 
 static char *hVarSubstExt(char *desc, struct cart *cart, struct trackDb *tdb, char *database,
-                          char *src)
+                          char *src, boolean hubHtml)
 /* Parse a string and substitute variable references.  Return NULL if
  * no variable references were found.  Error on missing variables (except
  * $matrix).  desc is a brief description to print on an error to help with
  * debugging. tdb maybe NULL to only do substitutions based on database
- * and organism.  cart may be NULL. See trackDb/README for more information.*/
+ * and organism.  cart may be NULL. See trackDb/README for more information.
+ * In hubHtml mode nothing is an error and only the variables in hubHtmlVars are
+ * recognized: every other `$' is copied through unchanged. */
 {
 struct dyString *dest = NULL;
 char *start = src;  // start of current static string in src
 char *next = src;   // cursor
 char varName[65];
 
 while ((next = strchr(next, '$')) != NULL)
     {
     if (dest == NULL)
         dest = dyStringNew(strlen(src));
     dyStringAppendN(dest, start, next-start);
     if (*(next+1) == '$')
         {
         // $$ is a literal $
         dyStringAppendC(dest, '$');
         start = next = next + 2;
         }
+    else if (hubHtml)
+        {
+        // variable reference, or just a dollar sign in the text
+        char *after = parseVarNameMaybe(next, varName, sizeof(varName));
+        if ((after != NULL) && isHubHtmlVar(cart, tdb, varName))
+            {
+            substVar(desc, cart, tdb, database, varName, dest);
+            start = next = after;
+            }
+        else
+            {
+            dyStringAppendC(dest, '$');
+            start = next = next + 1;
+            }
+        }
     else
         {
         // variable reference
         start = next = parseVarName(desc, next, varName, sizeof(varName));
         substVar(desc, cart, tdb, database, varName, dest);
         }
     }
 if (dest != NULL)
     {
     dyStringAppend(dest, start);
     return dyStringCannibalize(&dest);
     }
 else
     return NULL; // no substitutions
 }
 
 char *hVarSubst(char *desc, struct trackDb *tdb, char *database, char *src)
 /* Parse a string and substitute variable references.  Return NULL if
  * no variable references were found.  Error on missing variables (except
  * $matrix).  desc is a brief description to print on error to help with
  * debugging. tdb maybe NULL to only do substitutions based on database
  * and organism. See trackDb/README for more information.*/
 {
-return hVarSubstExt(desc, NULL, tdb, database, src);
+return hVarSubstExt(desc, NULL, tdb, database, src, FALSE);
 }
 
 void hVarSubstInVar(char *desc, struct trackDb *tdb, char *database, char **varPtr)
 /* hVarSubst on a dynamically allocated string, replacing string in substitutions
  * occur, freeing the old memory if necessary.  See hVarSubst for details.
  */
 {
-char *dest = hVarSubstExt(desc, NULL, tdb, database, *varPtr);
+char *dest = hVarSubstExt(desc, NULL, tdb, database, *varPtr, FALSE);
 if (dest != NULL)
     {
     freez(varPtr);
     *varPtr = dest;
     }
 }
 
 void hVarSubstTrackDb(struct trackDb *tdb, char *database)
 /* Substitute variables in trackDb shortLabel, longLabel, and html fields. */
 {
 hVarSubstInVar(tdb->track, tdb, database, &tdb->shortLabel);
 hVarSubstInVar(tdb->track, tdb, database, &tdb->longLabel);
 hVarSubstInVar(tdb->track, tdb, database, &tdb->html);
 }
 
 void hVarSubstWithCart(char *desc, struct cart *cart, struct trackDb *tdb, char *database,
                        char **varPtr)
 /* Like hVarSubstInVar, but if cart is non-NULL, $hgsid will be substituted. */
 {
-char *dest = hVarSubstExt(desc, cart, tdb, database, *varPtr);
+char *dest = hVarSubstExt(desc, cart, tdb, database, *varPtr, FALSE);
 if (dest != NULL)
     {
     freez(varPtr);
     *varPtr = dest;
     }
 }
+
+void hVarSubstTrackDbHtml(struct cart *cart, struct trackDb *tdb, char *database)
+/* Substitute variables in the description page of a hub track.  Native trackDb needs no
+ * such call: hgTrackDb already substituted the html when it loaded trackDb.  A hub's html
+ * comes straight off the hub's web server and has never been through substitution, so it
+ * is done here, at render time, where $db, $hgsid and $parentTrack resolve to the hub_<id>_
+ * names the CGIs actually use.  Only the variables in hubHtmlVars are recognized and
+ * nothing is an error, so a dollar sign in a description page that was not written with
+ * this in mind stays a dollar sign. */
+{
+if ((tdb == NULL) || isEmpty(tdb->html) || !isHubTrack(tdb->track))
+    return;
+char *dest = hVarSubstExt(tdb->track, cart, tdb, database, tdb->html, TRUE);
+if (dest != NULL)
+    {
+    freez(&tdb->html);
+    tdb->html = dest;
+    }
+}