7d56cd6635651dfd4c9926d062d92ad7b65a3e80
chmalee
  Wed Jun 3 14:52:52 2026 -0700
More myVariants changes: make the share to accept a comma-sep list of usernames, allow both the read/edit flag and the usernames setting to be editable after creation. When editing usernames, add a confirmation if the field is left blank that this will make the share viewable/editable by anyone with the link, refs #33808

diff --git src/hg/lib/myVariants.c src/hg/lib/myVariants.c
index 9829cf711f5..01cc7d321c5 100644
--- src/hg/lib/myVariants.c
+++ src/hg/lib/myVariants.c
@@ -1,1152 +1,1148 @@
 #include "common.h"
 #include "linefile.h"
 #include "dystring.h"
 #include "jksql.h"
 #include "myVariants.h"
 #include "myVariantsShare.h"
 #include "customTrack.h"
 #include "hdb.h"
 #include "hgConfig.h"
 #include "cheapcgi.h"
 #include "trashDir.h"
 #include "obscure.h"
 #include "wikiLink.h"
 
 char *myVariantsItemTypes[] = { "transcript", "snv", "cnv" };
 int myVariantsNumItemTypes = ArraySize(myVariantsItemTypes);
 
 char *myVariantsCnvTypes[] = {
     "deletion", "duplication", "insertion", "inversion",
     "translocation", "complex", "breakend"
 };
 int myVariantsNumCnvTypes = ArraySize(myVariantsCnvTypes);
 
 char *myVariantsCanonicalItemType(char *s)
 /* Return the matching canonical entry from myVariantsItemTypes (case-insensitive),
  * or NULL if s is empty or not in the allow-list. */
 {
 if (isEmpty(s))
     return NULL;
 int ix = stringArrayIx(s, myVariantsItemTypes, myVariantsNumItemTypes);
 return ix < 0 ? NULL : myVariantsItemTypes[ix];
 }
 
 char *myVariantsCanonicalCnvType(char *s)
 /* Return the matching canonical entry from myVariantsCnvTypes (case-insensitive),
  * or NULL if s is empty or not in the allow-list. */
 {
 if (isEmpty(s))
     return NULL;
 int ix = stringArrayIx(s, myVariantsCnvTypes, myVariantsNumCnvTypes);
 return ix < 0 ? NULL : myVariantsCnvTypes[ix];
 }
 
 boolean isMyVariantsType(char *type)
 /* TRUE if type names the myVariants custom-track type. NULL-safe. */
 {
 return sameOk(type, MYVARIANTS_TYPE);
 }
 
 boolean isMyVariantsTrack(char *trackName)
 /* TRUE if trackName is a myVariants custom track (own or shared). NULL-safe. */
 {
 return trackName != NULL && startsWith(MYVARIANTS_TRACK_PREFIX, trackName);
 }
 
 boolean isMyVariantsSharedTrack(char *trackName)
 /* TRUE if trackName is a myVariants shared custom track. NULL-safe. */
 {
 return trackName != NULL && startsWith(MYVARIANTS_SHARED_TRACK_PREFIX, trackName);
 }
 
 void myVariantsStaticLoad(char **row, struct myVariants *ret)
 /* Load a row from myVariants table into ret. The contents of ret will be replaced at the next call to this function. */
 {
 int sizeOne;
 ret->bin = sqlUnsigned(row[0]);
 ret->chrom = row[1];
 ret->chromStart = sqlUnsigned(row[2]);
 ret->chromEnd = sqlUnsigned(row[3]);
 ret->name = row[4];
 ret->score = sqlUnsigned(row[5]);
 safecpy(ret->strand, sizeof(ret->strand), row[6]);
 ret->thickStart = sqlUnsigned(row[7]);
 ret->thickEnd = sqlUnsigned(row[8]);
 ret->itemRgb = sqlUnsigned(row[9]);
 ret->blockCount = sqlUnsigned(row[10]);
 sqlSignedDynamicArray(row[11], &ret->blockSizes, &sizeOne);
 assert(sizeOne == ret->blockCount);
 sqlSignedDynamicArray(row[12], &ret->chromStarts, &sizeOne);
 assert(sizeOne == ret->blockCount);
 ret->description = row[13];
 ret->db = row[14];
 ret->ref = row[15];
 ret->alt = row[16];
 ret->project = row[17];
 ret->mouseover = row[18];
 ret->itemType = row[19];
 ret->cnvType = row[20];
 ret->id = sqlUnsigned(row[21]);
 }
 
 struct myVariants *myVariantsLoadByQuery(struct sqlConnection *conn, char *query)
 /* Load all myVariants from table that satisfy the query given. Dispose of this with myVariantsFreeList(). */
 {
 struct myVariants *list = NULL, *el;
 struct sqlResult *sr;
 char **row;
 sr = sqlGetResult(conn, query);
 while ((row = sqlNextRow(sr)) != NULL)
     {
     el = myVariantsLoad(row);
     slAddHead(&list, el);
     }
 slReverse(&list);
 sqlFreeResult(&sr);
 return list;
 }
 
 static char *commaIntList(int *arr, int n)
 /* Build a "n1,n2,...,nN," string from an int array.  Caller frees. */
 {
 struct dyString *dy = dyStringNew(n * 8);
 int i;
 for (i = 0; i < n; i++)
     dyStringPrintf(dy, "%d,", arr[i]);
 return dyStringCannibalize(&dy);
 }
 
 void myVariantsSaveToDb(struct sqlConnection *conn, struct myVariants *el, char *tableName, int updateSize)
 /* Save myVariants as a row to the table specified by tableName.
  * Uses explicit column names so custom fields in el->customFields are included.
  * If el->name is NULL or empty, fills it in post-INSERT as "Variant N" using
  * the row's auto-increment id; sqlLastAutoId wraps MariaDB's mysql_insert_id,
  * which is per-connection and unaffected by concurrent INSERTs on other
  * connections. */
 {
 struct dyString *update = dyStringNew(updateSize);
 sqlDyStringPrintf(update, "insert into %s (bin,chrom,chromStart,chromEnd,name,score,strand,thickStart,thickEnd,itemRgb,blockCount,blockSizes,chromStarts,description,db,ref,alt,project,mouseover,itemType,cnvType", tableName);
 
 /* Append custom field column names */
 struct slPair *cf;
 for (cf = el->customFields; cf != NULL; cf = cf->next)
     sqlDyStringPrintf(update, ",%s", cf->name);
 
 char *insertName = isEmpty(el->name) ? "" : el->name;
 char *blockSizesStr = commaIntList(el->blockSizes, el->blockCount);
 char *chromStartsStr = commaIntList(el->chromStarts, el->blockCount);
 sqlDyStringPrintf(update, ") values (%u,'%s',%u,%u,'%s',%u,'%s',%u,%u,%u,%u,'%s','%s','%s','%s','%s','%s','%s','%s','%s','%s'",
     el->bin, el->chrom, el->chromStart, el->chromEnd, insertName, el->score, el->strand,
     el->thickStart, el->thickEnd, el->itemRgb, el->blockCount, blockSizesStr, chromStartsStr,
     el->description, el->db, el->ref, el->alt, el->project, el->mouseover,
     isEmpty(el->itemType) ? "snv" : el->itemType,
     isEmpty(el->cnvType) ? "" : el->cnvType);
 freeMem(blockSizesStr);
 freeMem(chromStartsStr);
 
 /* Append custom field values */
 for (cf = el->customFields; cf != NULL; cf = cf->next)
     sqlDyStringPrintf(update, ",'%s'", (char *)cf->val);
 
 sqlDyStringPrintf(update, ")");
 sqlUpdate(conn, update->string);
 dyStringFree(&update);
 
 if (isEmpty(el->name))
     {
     unsigned int newId = sqlLastAutoId(conn);
     struct dyString *nameUpdate = sqlDyStringCreate(
         "update %s set name = 'Variant %u' where id = %u",
         tableName, newId, newId);
     sqlUpdate(conn, dyStringCannibalize(&nameUpdate));
     el->id = newId;
     freez(&el->name);
     struct dyString *dy = dyStringNew(0);
     dyStringPrintf(dy, "Variant %u", newId);
     el->name = dyStringCannibalize(&dy);
     }
 }
 
 struct myVariants *myVariantsLoad(char **row)
 /* Load a myVariants from row fetched with select * from myVariants from database. Dispose of this with myVariantsFree(). */
 {
 struct myVariants *ret;
 AllocVar(ret);
 int sizeOne;
 ret->bin = sqlUnsigned(row[0]);
 ret->chrom = cloneString(row[1]);
 ret->chromStart = sqlUnsigned(row[2]);
 ret->chromEnd = sqlUnsigned(row[3]);
 ret->name = cloneString(row[4]);
 ret->score = sqlUnsigned(row[5]);
 safecpy(ret->strand, sizeof(ret->strand), row[6]);
 ret->thickStart = sqlUnsigned(row[7]);
 ret->thickEnd = sqlUnsigned(row[8]);
 ret->itemRgb = sqlUnsigned(row[9]);
 ret->blockCount = sqlUnsigned(row[10]);
 sqlSignedDynamicArray(row[11], &ret->blockSizes, &sizeOne);
 assert(sizeOne == ret->blockCount);
 sqlSignedDynamicArray(row[12], &ret->chromStarts, &sizeOne);
 assert(sizeOne == ret->blockCount);
 ret->description = cloneString(row[13]);
 ret->db = cloneString(row[14]);
 ret->ref = cloneString(row[15]);
 ret->alt = cloneString(row[16]);
 ret->project = cloneString(row[17]);
 ret->mouseover = cloneString(row[18]);
 ret->itemType = cloneString(row[19]);
 ret->cnvType = cloneString(row[20]);
 ret->id = sqlUnsigned(row[21]);
 return ret;
 }
 
 struct myVariants *myVariantsLoadAll(char *fileName)
 /* Load all myVariants from a whitespace-separated file. Dispose of this with myVariantsFreeList(). */
 {
 struct myVariants *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
 char *row[MYVARIANTS_NUM_COLS];
 while (lineFileRow(lf, row))
     {
     el = myVariantsLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct myVariants *myVariantsLoadAllByChar(char *fileName, char chopper)
 /* Load all myVariants from a chopper separated file. Dispose of this with myVariantsFreeList(). */
 {
 struct myVariants *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
 char *row[MYVARIANTS_NUM_COLS];
 while (lineFileNextCharRow(lf, chopper, row, ArraySize(row)))
     {
     el = myVariantsLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct myVariants *myVariantsCommaIn(char **pS, struct myVariants *ret)
 /* Create a myVariants out of a comma separated string. This will fill in ret if non-null, otherwise will return a new myVariants */
 {
 char *s = *pS;
 if (ret == NULL)
     AllocVar(ret);
 ret->bin = sqlUnsignedComma(&s);
 ret->chrom = sqlStringComma(&s);
 ret->chromStart = sqlUnsignedComma(&s);
 ret->chromEnd = sqlUnsignedComma(&s);
 ret->name = sqlStringComma(&s);
 ret->score = sqlUnsignedComma(&s);
 sqlFixedStringComma(&s, ret->strand, sizeof(ret->strand));
 ret->thickStart = sqlUnsignedComma(&s);
 ret->thickEnd = sqlUnsignedComma(&s);
 ret->itemRgb = sqlUnsignedComma(&s);
 ret->blockCount = sqlUnsignedComma(&s);
     {
     int i;
     s = sqlEatChar(s, '{');
     if (ret->blockCount > 0)
         AllocArray(ret->blockSizes, ret->blockCount);
     for (i=0; i<ret->blockCount; ++i)
         ret->blockSizes[i] = sqlSignedComma(&s);
     s = sqlEatChar(s, '}');
     s = sqlEatChar(s, ',');
     }
     {
     int i;
     s = sqlEatChar(s, '{');
     if (ret->blockCount > 0)
         AllocArray(ret->chromStarts, ret->blockCount);
     for (i=0; i<ret->blockCount; ++i)
         ret->chromStarts[i] = sqlSignedComma(&s);
     s = sqlEatChar(s, '}');
     s = sqlEatChar(s, ',');
     }
 ret->description = sqlStringComma(&s);
 ret->db = sqlStringComma(&s);
 ret->ref = sqlStringComma(&s);
 ret->alt = sqlStringComma(&s);
 ret->project = sqlStringComma(&s);
 ret->mouseover = sqlStringComma(&s);
 ret->itemType = sqlStringComma(&s);
 ret->cnvType = sqlStringComma(&s);
 ret->id = sqlUnsignedComma(&s);
 *pS = s;
 return ret;
 }
 
 void myVariantsFree(struct myVariants **pEl)
 /* Free a single dynamically allocated myVariants such as created with myVariantsLoad(). */
 {
 struct myVariants *el;
 if ((el = *pEl) == NULL) return;
 freeMem(el->chrom);
 freeMem(el->name);
 freeMem(el->blockSizes);
 freeMem(el->chromStarts);
 freeMem(el->description);
 freeMem(el->db);
 freeMem(el->ref);
 freeMem(el->alt);
 freeMem(el->project);
 freeMem(el->mouseover);
 freeMem(el->itemType);
 freeMem(el->cnvType);
 slPairFreeValsAndList(&el->customFields);
 freez(pEl);
 }
 
 void myVariantsFreeList(struct myVariants **pList)
 /* Free a list of dynamically allocated myVariants's */
 {
 struct myVariants *el, *next;
 for (el = *pList; el != NULL; el = next)
     {
     next = el->next;
     myVariantsFree(&el);
     }
 *pList = NULL;
 }
 
 void myVariantsOutput(struct myVariants *el, FILE *f, char sep, char lastSep)
 /* Print out myVariants. Separate fields with sep. Follow last field with lastSep. */
 {
 fprintf(f, "%u", el->bin);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->chrom);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->chromStart);
 fputc(sep,f);
 fprintf(f, "%u", el->chromEnd);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->name);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->score);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->strand);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->thickStart);
 fputc(sep,f);
 fprintf(f, "%u", el->thickEnd);
 fputc(sep,f);
 fprintf(f, "%u", el->itemRgb);
 fputc(sep,f);
 fprintf(f, "%u", el->blockCount);
 fputc(sep,f);
     {
     int i;
     if (sep == ',') fputc('{',f);
     for (i=0; i<el->blockCount; ++i)
         {
         fprintf(f, "%d", el->blockSizes[i]);
         fputc(',', f);
         }
     if (sep == ',') fputc('}',f);
     }
 fputc(sep,f);
     {
     int i;
     if (sep == ',') fputc('{',f);
     for (i=0; i<el->blockCount; ++i)
         {
         fprintf(f, "%d", el->chromStarts[i]);
         fputc(',', f);
         }
     if (sep == ',') fputc('}',f);
     }
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->description);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->db);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->ref);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->alt);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->project);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->mouseover);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->itemType ? el->itemType : "");
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->cnvType ? el->cnvType : "");
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->id);
 fputc(lastSep,f);
 }
 
 static char *myVariantsAutoSqlString =
 "table myVariants\n"
 "\"An item in a myVariants type track.\"\n"
 "    (\n"
 "    uint bin;         \"Bin for range index\"\n"
 "    string chrom;     \"Reference sequence chromosome or scaffold\"\n"
 "    uint   chromStart;\"Start position in chromosome\"\n"
 "    uint   chromEnd;  \"End position in chromosome\"\n"
 "    string name;      \"Name of item - up to 16 chars\"\n"
 "    uint  score;      \"0-1000.  Higher numbers are darker.\"\n"
 "    char[1] strand;   \"+ or - for strand\"\n"
 "    uint   thickStart;\"Start of thick part\"\n"
 "    uint   thickEnd;  \"End position of thick part\"\n"
 "    uint itemRgb;     \"RGB 8 bits each as in bed\"\n"
 "    uint blockCount;  \"Number of blocks\"\n"
 "    int[blockCount] blockSizes; \"Comma separated list of block sizes\"\n"
 "    int[blockCount] chromStarts; \"Start positions relative to chromStart\"\n"
 "    lstring description; \"Longer item description\"\n"
 "    string db;        \"database name of this annotation\"\n"
 "    string ref;       \"reference allele\"\n"
 "    string alt;       \"alternate allele\"\n"
 "    string project;    \"project name for grouping variants\"\n"
 "    string mouseover;  \"short mouseover text for hover display\"\n"
 "    uint id;          \"Unique ID for item\"\n"
 "    )\n"
 ;
 
 struct asObject *myVariantsAsObj()
 /* Return asObject describing fields of myVariants */
 {
 return asParseText(myVariantsAutoSqlString);
 }
 
 char *myVariantsGetDatabaseForUser(char *userName)
 /* Hash the userName and map it to 1..31 inclusive for deciding what
  * database the table should be created in */
 {
 if (!userName)
     return NULL;
 
 unsigned hashed = hashString(userName);
 unsigned clamped = (hashed % 31) + 1;
 struct dyString *dbName = dyStringCreate("customData%02u", clamped);
 return dyStringCannibalize(&dbName);
 }
 
 static char *encodeUserNameForIdentifier(char *userName)
 /* Return a SQL-identifier-safe encoding of userName.  Alphanumeric and
  * underscore characters are kept verbatim; every other byte becomes _XX where
  * XX is its lowercase two-digit hex value.  This keeps the encoding
  * deterministic, reversible, and free of collisions for distinct inputs that
  * differ only by special characters.  Caller frees. */
 {
 if (isEmpty(userName))
     return cloneString("");
 struct dyString *dy = dyStringNew(strlen(userName) + 8);
 char *p;
 for (p = userName; *p; p++)
     {
     unsigned char c = (unsigned char)*p;
     if (isalnum(c) || c == '_')
         dyStringAppendC(dy, c);
     else
         dyStringPrintf(dy, "_%02x", c);
     }
 return dyStringCannibalize(&dy);
 }
 
 char *myVariantsGetTableName(char *userName)
 /* Build the SQL table name for this user's myVariants.  Encodes the user
  * name so non-identifier characters (e.g. '@' in email-style logins) don't
  * fail sqlCheckIdentifier. */
 {
 if (!userName)
     return NULL;
 char *encoded = encodeUserNameForIdentifier(userName);
 struct dyString *tableName = dyStringCreate(MYVARIANTS_TRACK_PREFIX "%s", encoded);
 freeMem(encoded);
 return dyStringCannibalize(&tableName);
 }
 
 char *myVariantsGetDbTable(char *userName)
 /* Return the string db.tableName based on the userName for use in sql statements
  * without specifying the database */
 {
 char *db = myVariantsGetDatabaseForUser(userName);
 char *tbl = myVariantsGetTableName(userName);
 if (isNotEmpty(db) && isNotEmpty(tbl))
     {
     struct dyString *ret = dyStringCreate("%s.%s", db, tbl);
     return dyStringCannibalize(&ret);
     }
 return NULL;
 }
 
 void myVariantsDeleteForDb(char *userName, char *targetDb)
 /* Delete the user's myVariants items for the given assembly.  No-op if the
  * table doesn't exist. */
 {
 if (isEmpty(userName) || isEmpty(targetDb))
     return;
 char *dbTable = myVariantsTableExists(userName);
 if (isEmpty(dbTable))
     return;
 struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
 struct dyString *del = sqlDyStringCreate("DELETE FROM %s WHERE db='%s'",
     dbTable, targetDb);
 sqlUpdate(conn, del->string);
 dyStringFree(&del);
 hFreeConn(&conn);
 }
 
 struct myVariantsShare *myVariantsResolveSharedTrack(char *trackName, struct cart *cart)
 /* For a "myVariants_shared_*" custom-track name, look up and revalidate the
  * share record from hgcentral. Returns NULL if the track is not a shared
  * track, the cart cookie is missing, the share has been revoked, or the
  * current user is not authorized (target user mismatch). The returned share
  * carries the validated owner/db/project/permission; callers should use these
  * (not the cart-supplied values) for authorization or scoping decisions.
  * Caller frees with myVariantsShareFree. */
 {
 if (isEmpty(trackName) || !isMyVariantsSharedTrack(trackName))
     return NULL;
 char *token = trackName + strlen(MYVARIANTS_SHARED_TRACK_PREFIX);
 char cartVar[256];
 safef(cartVar, sizeof(cartVar), MYVAR_SHARED_CART_PREFIX "%s", token);
 /* The cart-cookie presence gate is belt-and-suspenders: it ensures the share
  * was once accepted into this session before we hit hgcentral. The real
  * authorization is the targetUser check below against the live share row. */
 if (cart == NULL || !cartVarExists(cart, cartVar))
     return NULL;
 struct sqlConnection *conn = hConnectCentral();
 if (!sqlTableExists(conn, "myVariantsShares"))
     {
     hDisconnectCentral(&conn);
     return NULL;
     }
 struct myVariantsShare *share = myVariantsGetShareByToken(conn, token);
 hDisconnectCentral(&conn);
 if (share == NULL)
     return NULL;
-if (isNotEmpty(share->targetUser))
-    {
-    char *userName = getUserName();
-    if (isEmpty(userName) || !sameString(share->targetUser, userName))
+if (!myVariantsShareAllowsUser(share, getUserName()))
     {
     myVariantsShareFree(&share);
     return NULL;
     }
-    }
 return share;
 }
 
 char *myVariantsResolveDbTableForCustomTrack(char *trackName, struct cart *cart)
 /* For a custom-track name of the form "myVariants_*", return the fully
  * qualified SQL table (db.tableName) holding the items.  Handles both own
  * tracks and shared tracks. For shared tracks, revalidates the share against
  * hgcentral; returns NULL if the share has been revoked, downgraded out of
  * scope, or is not for the current user. */
 {
 if (isEmpty(trackName))
     return NULL;
 if (isMyVariantsSharedTrack(trackName))
     {
     struct myVariantsShare *share = myVariantsResolveSharedTrack(trackName, cart);
     if (share == NULL)
         return NULL;
     char *dbTable = myVariantsGetDbTable(share->ownerUser);
     myVariantsShareFree(&share);
     return dbTable;
     }
 if (isMyVariantsTrack(trackName))
     {
     /* trackName is the SQL-identifier-encoded form "myVariants_<encoded>".
      * Resolve via the current logged-in user (an own track is only viewable
      * by its owner) and verify the trackName matches the encoded form for
      * that user before returning their db.tableName. */
     char *userName = getUserName();
     if (isEmpty(userName))
         return NULL;
     char *expected = myVariantsGetTableName(userName);
     boolean match = sameOk(expected, trackName);
     freeMem(expected);
     if (!match)
         return NULL;
     return myVariantsGetDbTable(userName);
     }
 return NULL;
 }
 
 char *myVariantsSharedScopeWhere(char *trackName, struct cart *cart)
 /* For a "myVariants_shared_*" custom-track, return a SQL WHERE-clause
  * fragment that limits a query to the share's authorized project and db
  * (e.g. "db='hg38' and project='Variants'", or "db='hg38'" alone when the
  * share's project is "*"). Returns NULL for non-shared tracks or revoked
  * shares. Memoized per-process: callers receive a fresh cloneString that
  * they own. */
 {
 static struct hash *cache = NULL;
 if (cache == NULL)
     cache = hashNew(0);
 if (isEmpty(trackName) || !isMyVariantsSharedTrack(trackName))
     return NULL;
 char *cached = hashFindVal(cache, trackName);
 if (cached != NULL)
     return cached[0] ? cloneString(cached) : NULL;
 
 char *result = NULL;
 struct myVariantsShare *share = myVariantsResolveSharedTrack(trackName, cart);
 if (share != NULL && isNotEmpty(share->db))
     {
     struct dyString *dy = sqlDyStringCreate("db='%s'", share->db);
     if (isNotEmpty(share->project) && !sameString(share->project, "*"))
         sqlDyStringPrintf(dy, " and project='%s'", share->project);
     result = dyStringCannibalize(&dy);
     }
 myVariantsShareFree(&share);
 hashAdd(cache, trackName, cloneString(result ? result : ""));
 return result;
 }
 
 void myVariantsStripHiddenFields(struct slName **pFieldList)
 /* Remove any field whose name starts with "_hidden_" from the list in place.
  * Handles bare names ("_hidden_foo") and dotted/qualified names
  * ("db.table._hidden_foo") by checking the segment after the last '.'.
  * Used by hgTables for myVariants_shared_* tables so a recipient does not
  * see columns the owner has hidden. (Custom non-hidden columns are kept,
  * since they are part of the data the owner intentionally shared.) */
 {
 struct slName *kept = NULL, *fld, *next;
 for (fld = *pFieldList; fld != NULL; fld = next)
     {
     next = fld->next;
     char *bare = strrchr(fld->name, '.');
     bare = bare ? bare + 1 : fld->name;
     if (startsWith("_hidden_", bare))
         freeMem(fld);
     else
         slAddHead(&kept, fld);
     }
 slReverse(&kept);
 *pFieldList = kept;
 }
 
 char *myVariantsTableExists(char *userName)
 /* See if we already have a table for this user. If so, return the name
  * of the table (in db.tableName format), else NULL */
 {
 if (!userName)
     return NULL;
 char *dbTable = myVariantsGetDbTable(userName);
 if (!dbTable)
     return NULL;
 struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
 boolean exists = sqlTableExists(conn, dbTable);
 hFreeConn(&conn);
 if (exists)
     return dbTable;
 return NULL;
 }
 
 char *myVariantsCreateTable(char *userName)
 /* Return name of myVariants table (in db.tableName format) for user if it exists or
  * we can create, NULL otherwise */
 {
 if (!userName)
     return NULL;
 char *db = myVariantsGetDatabaseForUser(userName);
 char *tableName = myVariantsGetTableName(userName);
 if (!tableName)
     return NULL;
 char *dbTable = myVariantsGetDbTable(userName);
 struct sqlConnection *existConn = hAllocConn(CUSTOM_TRASH);
 boolean exists = sqlTableExists(existConn, dbTable);
 hFreeConn(&existConn);
 if (exists)
     return dbTable;
 else
     {
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     struct dyString *createTable = sqlDyStringCreate(
             "CREATE TABLE %s.%s (\n"
             "    bin int unsigned not null,\n"
             "    chrom varchar(255) not null,\n"
             "    chromStart int unsigned not null,\n"
             "    chromEnd int unsigned not null,\n"
             "    name varchar(255) not null,\n"
             "    score int unsigned not null,\n"
             "    strand char(1) not null,\n"
             "    thickStart int unsigned not null,\n"
             "    thickEnd int unsigned not null,\n"
             "    itemRgb int unsigned not null,\n"
             "    blockCount int unsigned not null,\n"
             "    blockSizes longblob not null,\n"
             "    chromStarts longblob not null,\n"
             "    description longblob not null,\n"
             "    db varchar(255) not null,\n"
             "    ref varchar(255) not null,\n"
             "    alt varchar(255) not null,\n"
             "    project varchar(255) not null,\n"
             "    mouseover varchar(255) not null,\n"
             "    itemType varchar(16) not null default 'snv',\n"
             "    cnvType varchar(32) not null default '',\n"
             "    id int auto_increment,\n"
             "    PRIMARY KEY(id),\n"
             "    INDEX(chrom(16),bin),\n"
             "    INDEX(db),\n"
             "    INDEX(project)\n"
             ") ENGINE=InnoDB;", db, tableName);
     sqlUpdate(conn, dyStringCannibalize(&createTable));
     return myVariantsGetDbTable(userName);
     }
 } 
 
 
 static void readLabelsFromCtFile(char *path, char *trackName,
                                  char **retShort, char **retLong)
 /* Parse the matching track line in an existing ctfile and pull shortLabel
  * and longLabel via hashVarLine.  Leaves *ret as NULL when not found or
  * path is missing. */
 {
 *retShort = NULL;
 *retLong = NULL;
 if (isEmpty(path) || isEmpty(trackName) || !fileExists(path))
     return;
 struct lineFile *lf = lineFileOpen(path, TRUE);
 char *line = NULL;
 while (lineFileNext(lf, &line, NULL))
     {
     if (!startsWith("track ", line))
         continue;
     struct hash *settings = hashVarLine(line + strlen("track "), lf->lineIx);
     char *name = hashFindVal(settings, "name");
     if (name != NULL && sameString(name, trackName))
         {
         char *s = hashFindVal(settings, "shortLabel");
         char *l = hashFindVal(settings, "longLabel");
         if (s != NULL)
             *retShort = cloneString(s);
         if (l != NULL)
             *retLong = cloneString(l);
         hashFree(&settings);
         break;
         }
     hashFree(&settings);
     }
 lineFileClose(&lf);
 }
 
 static char *sanitizeLabel(char *raw, int maxLen)
 /* Strip both quote characters, newlines and carriage returns, then cap to
  * maxLen characters.  Returns NULL for NULL/empty input or when sanitizing
  * leaves nothing behind.  Matches the precedent in hgCustom.c for
  * user-supplied CT labels written into the trackline. */
 {
 if (isEmpty(raw))
     return NULL;
 char *s = cloneString(raw);
 stripChar(s, '"');
 stripChar(s, '\'');
 stripChar(s, '\\');
 stripChar(s, '\n');
 stripChar(s, '\r');
 if ((int)strlen(s) > maxLen)
     s[maxLen] = '\0';
 if (isEmpty(s))
     {
     freeMem(s);
     return NULL;
     }
 return s;
 }
 
 static void myVariantsCtFilePath(struct tempName *tn,
                                  char *encodedTableName, char *targetDb)
 /* Build the per-user-per-db ctfile path.  Prefers
  * cfgOption("myVariantsDataDir") so the file lives outside trash and the
  * trashCleaner doesn't expire it; falls back to trashDirReusableFile when
  * the dir isn't configured. */
 {
 char *persistentDir = cfgOption("myVariantsDataDir");
 if (isNotEmpty(persistentDir))
     {
     /* Group files per user so one user's tracks live together:
      * ${persistentDir}/<encodedTableName>/<db>.bed. */
     char *subdir = isNotEmpty(encodedTableName) ? encodedTableName : "shared";
     char *sep = endsWith(persistentDir, "/") ? "" : "/";
     char path[PATH_LEN];
     safef(path, sizeof path, "%s%s%s/%s.bed", persistentDir, sep, subdir, targetDb);
     char dirPart[PATH_LEN];
     splitPath(path, dirPart, NULL, NULL);
     makeDirsOnPath(dirPart);
     safef(tn->forCgi, sizeof tn->forCgi, "%s", path);
     safef(tn->forHtml, sizeof tn->forHtml, "%s", path);
     }
 else
     {
     char base[PATH_LEN];
     char *hostPort = cgiServerNamePort();
     safef(base, sizeof base, MYVARIANTS_TRACK_PREFIX "%s_%s_%s",
         hostPort ? hostPort : "localhost", targetDb,
         isNotEmpty(encodedTableName) ? encodedTableName : "shared");
     for (char *p = base; *p; p++) if (*p == '/') *p = '_';
     trashDirReusableFile(tn, "ct", base, ".bed");
     }
 }
 
 void myVariantsUnlinkCtFile(char *userName, char *targetDb)
 /* Delete the on-disk ctfile for this user+assembly if it exists.  Uses the
  * same path resolution as myVariantsWriteCtFile so it targets either the
  * persistent dir (myVariantsDataDir) or the trash fallback. */
 {
 if (isEmpty(userName) || isEmpty(targetDb))
     return;
 char *encodedTableName = myVariantsGetTableName(userName);
 if (isEmpty(encodedTableName))
     return;
 struct tempName tn;
 myVariantsCtFilePath(&tn, encodedTableName, targetDb);
 if (fileExists(tn.forCgi))
     unlink(tn.forCgi);
 freeMem(encodedTableName);
 }
 
 char *myVariantsWriteCtFile(char *userName, char *targetDb, struct cart *cart)
 /* Write a Custom Track file for user's myVariants in targetDb and any shared
  * tracks found in cart. Return filename or NULL if nothing to write.
  * If cfgOption("myVariantsDataDir") is set the file is placed there so the
  * trashCleaner doesn't expire it; otherwise it goes in trash/ct as before. */
 {
 if (isEmpty(targetDb))
     return NULL;
 
 /* Identifier-safe form of userName for the CT track name and trash filename.
  * The raw userName may contain non-ASCII or characters unsafe in trackDb
  * syntax, filesystem paths, or SQL (e.g. '@', ';', spaces, quotes). */
 char *encodedTableName = NULL;
 if (isNotEmpty(userName))
     encodedTableName = myVariantsGetTableName(userName);
 
 /* Check if user has their own items */
 boolean hasOwnItems = FALSE;
 if (isNotEmpty(userName))
     {
     char *dbTable = myVariantsGetDbTable(userName);
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     if (isNotEmpty(dbTable) && sqlTableExists(conn, dbTable))
         {
         char countQuery[512];
         sqlSafef(countQuery, sizeof countQuery,
             "select count(*) from %s where db='%s'", dbTable, targetDb);
         hasOwnItems = (sqlQuickNum(conn, countQuery) > 0);
         }
     hFreeConn(&conn);
     }
 
 /* Collect shared track lines from cart. All authoritative metadata
  * (owner/db/project/label) comes from the live share row, never from
  * cart-parsed values. */
 struct dyString *sharedLines = dyStringNew(0);
 if (cart != NULL)
     {
     struct hashEl *shareVars = cartFindPrefix(cart, MYVAR_SHARED_CART_PREFIX);
     struct hashEl *el;
     for (el = shareVars; el != NULL; el = el->next)
         {
         char *token = el->name + strlen(MYVAR_SHARED_CART_PREFIX);
         char trackName[512];
         safef(trackName, sizeof(trackName), MYVARIANTS_SHARED_TRACK_PREFIX "%s", token);
         struct myVariantsShare *share = myVariantsResolveSharedTrack(trackName, cart);
         if (share == NULL)
             continue;
         if (!sameString(share->db, targetDb))
             {
             myVariantsShareFree(&share);
             continue;
             }
         /* Skip if the sharer is the current user - they already see their own track */
         if (isNotEmpty(userName) && sameString(share->ownerUser, userName))
             {
             myVariantsShareFree(&share);
             continue;
             }
         /* Verify the owner's table exists */
         char *ownerDbTable = myVariantsGetDbTable(share->ownerUser);
         struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
         boolean tableOk = (isNotEmpty(ownerDbTable) && sqlTableExists(conn, ownerDbTable));
         hFreeConn(&conn);
         if (!tableOk)
             {
             myVariantsShareFree(&share);
             continue;
             }
         /* Strip double quotes from owner-controlled values to prevent injection
          * of additional trackDb settings via the CT file track line. */
         char *owner = cloneString(share->ownerUser);
         char *project = cloneString(share->project);
         char *label = isNotEmpty(share->label) ? cloneString(share->label) : NULL;
         stripChar(owner, '"');
         stripChar(project, '"');
         if (label != NULL)
             stripChar(label, '"');
         char *projectLabel = sameString(project, "*") ? "All" : project;
         char shortLabel[64];
         if (isNotEmpty(label))
             safef(shortLabel, sizeof(shortLabel), "%s", label);
         else
             safef(shortLabel, sizeof(shortLabel), "%s's %s", owner, projectLabel);
         dyStringPrintf(sharedLines,
             "track name=\"" MYVARIANTS_SHARED_TRACK_PREFIX "%s\""
             " type=\"" MYVARIANTS_TYPE "\" itemRgb=\"on\""
             " visibility=\"pack\""
             " shortLabel=\"%s\""
             " longLabel=\"Shared annotations: %s (from %s)\"\n",
             token, shortLabel, projectLabel, owner);
         freeMem(owner);
         freeMem(project);
         freeMem(label);
         myVariantsShareFree(&share);
         }
     hashElFreeList(&shareVars);
     }
 
 if (!hasOwnItems && dyStringLen(sharedLines) == 0)
     {
     /* Nothing to write: remove any stale on-disk file so it doesn't leak
      * when the user empties their table.  Under the old trash-only path
      * the trashCleaner would have done this for us. */
     if (isNotEmpty(encodedTableName))
         {
         struct tempName stale;
         myVariantsCtFilePath(&stale, encodedTableName, targetDb);
         if (fileExists(stale.forCgi))
             unlink(stale.forCgi);
         }
     dyStringFree(&sharedLines);
     freeMem(encodedTableName);
     return NULL;
     }
 
 /* Reusable, stable filename per user+db - always rewrite since shares are dynamic */
 struct tempName tn;
 myVariantsCtFilePath(&tn, encodedTableName, targetDb);
 
 /* Resolve display labels for the user's own track: cart vars set by the
  * rename UI win, then the existing on-disk file (so a rename survives a
  * cart reset), then the default. */
 char *shortLabel = NULL, *longLabel = NULL;
 if (hasOwnItems)
     {
     if (cart != NULL)
         {
         char cartVar[256];
         safef(cartVar, sizeof cartVar, "%s.%s.shortLabel",
             encodedTableName, targetDb);
         shortLabel = sanitizeLabel(cartOptionalString(cart, cartVar), 80);
         safef(cartVar, sizeof cartVar, "%s.%s.longLabel",
             encodedTableName, targetDb);
         longLabel = sanitizeLabel(cartOptionalString(cart, cartVar), 200);
         }
     if (shortLabel == NULL || longLabel == NULL)
         {
         char *diskShort = NULL, *diskLong = NULL;
         readLabelsFromCtFile(tn.forCgi, encodedTableName, &diskShort, &diskLong);
         if (shortLabel == NULL)
             shortLabel = sanitizeLabel(diskShort, 80);
         if (longLabel == NULL)
             longLabel = sanitizeLabel(diskLong, 200);
         freeMem(diskShort);
         freeMem(diskLong);
         }
     if (shortLabel == NULL)
         shortLabel = cloneString("My Annotations");
     if (longLabel == NULL)
         longLabel = cloneString("My Annotations");
     }
 
 FILE *f = mustOpen(tn.forCgi, "w");
 if (hasOwnItems)
     fprintf(f, "track name=\"%s\" type=\"" MYVARIANTS_TYPE "\" itemRgb=\"on\""
         " visibility=\"pack\" shortLabel=\"%s\""
         " longLabel=\"%s\"\n", encodedTableName, shortLabel, longLabel);
 if (dyStringLen(sharedLines) > 0)
     fprintf(f, "%s", dyStringContents(sharedLines));
 carefulClose(&f);
 dyStringFree(&sharedLines);
 freeMem(encodedTableName);
 freeMem(shortLabel);
 freeMem(longLabel);
 return cloneString(tn.forCgi);
 }
 
 boolean myVariantsHandleCtRemoval(struct customTrack *ct, struct cart *cart,
                                   char *database)
 /* If ct is a myVariants own track: delete the user's rows for the current
  * assembly, unlink the persistent ctfile, drop the per-db renamed labels
  * and the visibility var.  If ct is a myVariants shared track: drop the
  * share-acceptance cart var and the visibility var.  Returns TRUE when ct
  * was a myVariants track and this function fully handled the cart cleanup
  * (caller must skip its own per-track cart cleanup so other-assembly
  * labels are preserved); FALSE otherwise. */
 {
 if (ct == NULL || ct->tdb == NULL || isEmpty(ct->tdb->track))
     return FALSE;
 char *trackName = ct->tdb->track;
 if (!isMyVariantsTrack(trackName))
     return FALSE;
 
 /* Cleanup common to any myVariants removal: drop the track's visibility var
  * and invalidate the on-disk ctfile pointer so the next entry-point visit
  * regenerates from current SQL/share state. */
 cartRemove(cart, trackName);
 char mvVar[256];
 safef(mvVar, sizeof mvVar, MYVARIANTS_FILE_VAR_PREFIX "%s", database);
 cartRemove(cart, mvVar);
 
 if (isMyVariantsSharedTrack(trackName))
     {
     /* Drop the share-acceptance var so the share isn't re-imported. */
     char *token = trackName + strlen(MYVARIANTS_SHARED_TRACK_PREFIX);
     char shareCartVar[256];
     safef(shareCartVar, sizeof shareCartVar,
         MYVAR_SHARED_CART_PREFIX "%s", token);
     cartRemove(cart, shareCartVar);
     return TRUE;
     }
 
 /* Own track: delete the user's SQL rows for this assembly, unlink the
  * persisted ctfile, and drop any renamed-label cart vars so a freshly
  * created track on this assembly starts at "My Annotations" again. */
 char *userName = wikiLinkUserName();
 if (isNotEmpty(userName))
     {
     myVariantsDeleteForDb(userName, database);
     myVariantsUnlinkCtFile(userName, database);
     }
 char labelPrefix[256];
 safef(labelPrefix, sizeof labelPrefix, "%s.%s.", trackName, database);
 cartRemovePrefix(cart, labelPrefix);
 return TRUE;
 }
 
 struct slName *myVariantsGetProjects(char *userName)
 /* Return list of distinct non-empty project values for this user's myVariants table.
  * Caller must slFreeList the result. Returns NULL if no projects or table doesn't exist. */
 {
 if (isEmpty(userName))
     return NULL;
 
 char *dbTable = myVariantsTableExists(userName);
 if (isEmpty(dbTable))
     return NULL;
 
 struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
 struct slName *projects = NULL;
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT DISTINCT project FROM %s WHERE project IS NOT NULL AND project != '' ORDER BY project",
     dbTable);
 projects = sqlQuickList(conn, query);
 hFreeConn(&conn);
 return projects;
 }
 
 /* Built-in column names from myVariants.as - any column NOT in this list is a
  * user-added custom column.  Filtering by name (rather than index) is robust
  * against column reordering or future schema changes. */
 static const char *builtInColumns[] = {
     "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand",
     "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes",
     "chromStarts", "description", "db", "ref", "alt", "project",
     "mouseover", "itemType", "cnvType", "id",
 };
 
 static boolean isBuiltInColumn(char *name)
 {
 int i;
 for (i = 0;  i < ArraySize(builtInColumns);  i++)
     if (sameString(name, builtInColumns[i]))
         return TRUE;
 return FALSE;
 }
 
 struct slName *myVariantsGetCustomFields(char *userName)
 /* Return list of user-added custom column names for this user's myVariants table.
  * Excludes built-in columns and _hidden_ prefixed columns.
  * Caller must slFreeList the result. Returns NULL if no custom fields or table doesn't exist. */
 {
 if (isEmpty(userName))
     return NULL;
 
 char *dbTable = myVariantsTableExists(userName);
 if (isEmpty(dbTable))
     return NULL;
 
 struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
 struct slName *allFields = sqlListFields(conn, dbTable);
 hFreeConn(&conn);
 if (allFields == NULL)
     return NULL;
 
 struct slName *customFields = NULL;
 struct slName *field;
 for (field = allFields; field != NULL; field = field->next)
     {
     if (isBuiltInColumn(field->name))
         continue;
     if (startsWith("_hidden_", field->name))
         continue;
     slNameAddHead(&customFields, field->name);
     }
 slFreeList(&allFields);
 slReverse(&customFields);
 return customFields;
 }
 
 struct slName *myVariantsGetHiddenFields(char *userName)
 /* Return list of hidden custom column names (with _hidden_ prefix stripped) for this user's
  * myVariants table. Caller must slFreeList the result. Returns NULL if none. */
 {
 if (isEmpty(userName))
     return NULL;
 
 char *dbTable = myVariantsTableExists(userName);
 if (isEmpty(dbTable))
     return NULL;
 
 struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
 struct slName *allFields = sqlListFields(conn, dbTable);
 hFreeConn(&conn);
 if (allFields == NULL)
     return NULL;
 
 struct slName *hiddenFields = NULL;
 struct slName *field;
 for (field = allFields; field != NULL; field = field->next)
     {
     if (isBuiltInColumn(field->name))
         continue;
     if (startsWith("_hidden_", field->name))
         slNameAddHead(&hiddenFields, field->name + strlen("_hidden_"));
     }
 slFreeList(&allFields);
 slReverse(&hiddenFields);
 return hiddenFields;
 }