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/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c
index 791e2b3d0f6..ca51d61da38 100644
--- src/hg/hgTracks/myVariantsTrack.c
+++ src/hg/hgTracks/myVariantsTrack.c
@@ -1,1312 +1,1392 @@
 /* myVariantsTrack.c - supports tracks of type myVariants.  Users can drag to create an item
  * and click to edit one. */
 
 /* Copyright (C) 2013 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "hash.h"
 #include "linefile.h"
 #include "jksql.h"
 #include "hdb.h"
 #include "hgTracks.h"
 #include "bed.h"
 #include "binRange.h"
 #include "myVariants.h"
 #include "myVariantsShare.h"
 #include "jsonParse.h"
 #include "sqlNum.h"
 #include "customFactory.h"
 #include "hgConfig.h"
 #include "htmlColor.h"
 #include "wikiLink.h"
 #include "hgFind.h"
 #include "hgHgvs.h"
 #include "jsonWrite.h"
 
 static char *reservedFieldNames[] = {
     "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand",
     "thickStart", "thickEnd", "itemRgb", "blockCount", "blockSizes",
     "chromStarts", "description", "db", "ref", "alt", "project",
     "mouseover", "itemType", "cnvType", "id", NULL
 };
 
 static char *truncateSeq(char *seq, int maxLen)
 /* Return seq cloned and truncated to maxLen with "..." appended if longer.
  * NULL or empty input returns NULL. */
 {
 if (isEmpty(seq))
     return NULL;
 if (strlen(seq) <= (size_t)maxLen)
     return cloneString(seq);
 struct dyString *dy = dyStringNew(maxLen + 4);
 dyStringAppendN(dy, seq, maxLen);
 dyStringAppend(dy, "...");
 return dyStringCannibalize(&dy);
 }
 
 static char *changeTypePrefix(enum hgvsChangeType t)
 /* Map a non-substitution HGVS change type to its short label. */
 {
 switch (t)
     {
     case hgvsctDel: return "Del";
     case hgvsctDup: return "Dup";
     case hgvsctIns: return "Ins";
     case hgvsctInv: return "Inv";
     case hgvsctCon: return "Con";
     default: return NULL;
     }
 }
 
 static char *synthesizeItemName(char *ref, char *alt,
                                 enum hgvsChangeType changeType, char *changeSeq)
 /* Build a default name for a new myVariants item from its ref/alt or HGVS
  * change.  Returns a cloned string, or NULL when the row's auto-increment
  * id is needed (the SQL layer fills in "Variant N" after the INSERT). */
 {
 boolean haveRef = !isEmpty(ref);
 boolean haveAlt = !isEmpty(alt);
 struct dyString *dy = dyStringNew(0);
 if (haveRef && haveAlt)
     dyStringPrintf(dy, "%s>%s", ref, alt);
 else if (haveRef)
     dyStringPrintf(dy, "Ref: %s", ref);
 else if (haveAlt)
     dyStringPrintf(dy, "Alt: %s", alt);
 else
     {
     char *prefix = changeTypePrefix(changeType);
     if (prefix != NULL && !isEmpty(changeSeq))
         {
         char *trunc = truncateSeq(changeSeq, 10);
         dyStringPrintf(dy, "%s: %s", prefix, trunc);
         freeMem(trunc);
         }
     }
 if (dyStringIsEmpty(dy))
     {
     dyStringFree(&dy);
     return NULL;
     }
 return dyStringCannibalize(&dy);
 }
 
 static void extractHgvsChange(char *hgvsTerm,
                               char **retRef, char **retAlt,
                               enum hgvsChangeType *retType, char **retSeq)
 /* Parse an HGVS term and pull either ref/alt (substitution) or the change
  * type plus its asserted sequence (del/dup/ins/inv/con).  Each out parameter
  * is set to a freshly cloned string or to NULL/hgvsctUndefined when the
  * piece is unavailable. */
 {
 *retRef = NULL;
 *retAlt = NULL;
 *retType = hgvsctUndefined;
 *retSeq = NULL;
 if (isEmpty(hgvsTerm))
     return;
 struct hgvsVariant *hgvs = hgvsParseTerm(hgvsTerm);
 if (hgvs == NULL || isEmpty(hgvs->changes))
     return;
 struct dyString *dyError = dyStringNew(0);
 struct hgvsChange *change = hgvsParseNucleotideChange(hgvs->changes, hgvs->type, dyError);
 dyStringFree(&dyError);
 if (change == NULL)
     return;
 if (change->type == hgvsctSubst &&
     change->value.refAlt.altType == hgvsstSimple)
     {
     if (!isEmpty(change->value.refAlt.refSequence))
         *retRef = cloneString(change->value.refAlt.refSequence);
     char *altSeq = change->value.refAlt.altValue.seq;
     if (!isEmpty(altSeq))
         *retAlt = cloneString(altSeq);
     }
 else
     {
     *retType = change->type;
     switch (change->type)
         {
         case hgvsctDel:
         case hgvsctDup:
         case hgvsctInv:
             if (!isEmpty(change->value.refAlt.refSequence))
                 *retSeq = cloneString(change->value.refAlt.refSequence);
             break;
         case hgvsctIns:
         case hgvsctCon:
             if (change->value.refAlt.altType == hgvsstSimple &&
                 !isEmpty(change->value.refAlt.altValue.seq))
                 *retSeq = cloneString(change->value.refAlt.altValue.seq);
             break;
         default:
             break;
         }
     }
 }
 
 static boolean isValidFieldName(char *name)
 /* Validate that name matches [a-zA-Z_][a-zA-Z0-9_]*, is not a reserved column name,
  * and does not start with _hidden_ */
 {
 if (isEmpty(name))
     return FALSE;
 if (startsWith("_hidden_", name))
     return FALSE;
 /* Check first char is letter or underscore */
 if (!isalpha(name[0]) && name[0] != '_')
     return FALSE;
 /* Check remaining chars are alphanumeric or underscore */
 int i;
 for (i = 1; name[i] != '\0'; i++)
     {
     if (!isalnum(name[i]) && name[i] != '_')
         return FALSE;
     }
 /* Check against reserved names */
 int j;
 for (j = 0; reservedFieldNames[j] != NULL; j++)
     {
     if (sameString(name, reservedFieldNames[j]))
         return FALSE;
     }
 return TRUE;
 }
 
 static boolean hasTabOrNewline(char *s)
 /* TRUE if s contains \t, \n, or \r. */
 {
 return s != NULL && (strchr(s, '\t') != NULL ||
                      strchr(s, '\n') != NULL ||
                      strchr(s, '\r') != NULL);
 }
 
 static void validateItemBlocks(struct myVariants *item)
 /* Format item as a 12-column BED row and run it through loadAndValidateBed
  * (isCt=TRUE) for the canonical block checks.  lineFileAbort fires errAbort
  * on failure, the same error path used elsewhere in this file. */
 {
 /* Reject embedded tab/newline in chrom and name: lineFileNext would
  * truncate the row and chopByChar would leave trailing row[] slots
  * uninitialized for loadAndValidateBed to dereference. */
 if (hasTabOrNewline(item->chrom))
     errAbort("chrom contains illegal whitespace");
 if (hasTabOrNewline(item->name))
     errAbort("name contains illegal whitespace");
 
 struct dyString *dy = dyStringNew(256);
 int i;
 dyStringPrintf(dy, "%s\t%u\t%u\t%s\t%u\t%s\t%u\t%u\t%u\t%u\t",
     item->chrom, item->chromStart, item->chromEnd,
     isEmpty(item->name) ? "x" : item->name,
     item->score, item->strand, item->thickStart, item->thickEnd,
     item->itemRgb, item->blockCount);
 for (i = 0; i < item->blockCount; i++)
     dyStringPrintf(dy, "%d,", item->blockSizes[i]);
 dyStringAppendC(dy, '\t');
 for (i = 0; i < item->blockCount; i++)
     dyStringPrintf(dy, "%d,", item->chromStarts[i]);
 
 /* lineFileOnString takes the buffer by pointer, but lineFileClose does
  * not free it (the fd<0 branch skips the free).  Hold the pointer
  * separately and free it after lineFileClose. */
 char *buf = dyStringCannibalize(&dy);
 struct lineFile *lf = lineFileOnString("myVariantsBlocks", TRUE, buf);
 char *row[12];
 char *line = NULL;
 lineFileNext(lf, &line, NULL);
 int got = chopByChar(line, '\t', row, ArraySize(row));
 if (got != 12)
     errAbort("validateItemBlocks: chopped %d fields, expected 12", got);
 struct bed tmp;
 ZeroVar(&tmp);
 loadAndValidateBed(row, 12, 12, lf, &tmp, NULL, TRUE);
 lineFileClose(&lf);
 freeMem(buf);
 freeMem(tmp.blockSizes);
 freeMem(tmp.chromStarts);
 }
 
 static void ensureItemBlocks(struct myVariants *item)
 /* If item has no blocks, synthesize a single full-span block. */
 {
 if (item->blockCount > 0)
     return;
 item->blockCount = 1;
 AllocArray(item->blockSizes, 1);
 AllocArray(item->chromStarts, 1);
 item->blockSizes[0] = item->chromEnd - item->chromStart;
 item->chromStarts[0] = 0;
 }
 
 void myVariantsJsCommand(char *command, struct track *trackList, struct hash *trackHash)
 /* Execute some command sent to us from the javaScript.  All we know for sure is that
  * the first word of the command is "myVariants."  We expect it to be of format:
  *    myVariants <trackName> <jsonData>
  * where jsonData is a JSON string containing the item details */
 {
 if (!cfgOptionBooleanDefault("doMyVariants", FALSE))
     return;
 char *userName = getUserName();
 if (userName == NULL)
     {
     warn("You must be logged in to add an annotation.");
     return;
     }
 
 /* This command runs mysql commands so require the hgsids match to prevent CSRF. */
 char *suppliedHgsid = cgiOptionalString("hgsid");
 char *expectedHgsid = cartSessionId(cart);
 if (isEmpty(suppliedHgsid) || isEmpty(expectedHgsid)
     || !sameString(suppliedHgsid, expectedHgsid))
     errAbort("session token missing or invalid");
 
 /* Parse out command into local variables. */
 char *words[3];
 char *dupeCommand = cloneString(command);	/* For parsing. */
 int wordCount = chopByWhiteRespectDoubleQuotes(dupeCommand, words, ArraySize(words));
 if (wordCount != 3)
    errAbort("Expecting %d words in jsCommand '%s'", wordCount, command);
 char *jsonData = words[2];
 
 /* Parse JSON data */
 struct jsonElement *json = jsonParse(jsonData);
 if (json == NULL)
     errAbort("Invalid JSON data in command: %s", jsonData);
 
 /* Handle hideField command: rename column with _hidden_ prefix */
 char *hideFieldName = jsonOptionalStringField(json, "hideField", NULL);
 if (isNotEmpty(hideFieldName))
     {
     if (isValidFieldName(hideFieldName))
         {
         char *tableName = myVariantsTableExists(userName);
         if (tableName)
             {
             struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
             if (sqlFieldIndex(conn, tableName, hideFieldName) >= 0)
                 {
                 struct dyString *alterSql = sqlDyStringCreate(
                     "ALTER TABLE %s CHANGE COLUMN %s _hidden_%s varchar(255) DEFAULT NULL",
                     tableName, hideFieldName, hideFieldName);
                 sqlUpdate(conn, dyStringContents(alterSql));
                 dyStringFree(&alterSql);
                 }
             hFreeConn(&conn);
             }
         }
     freez(&dupeCommand);
     return;
     }
 
 /* Create a new item based on command. */
 struct myVariants *item;
 AllocVar(item);
 
 /* Extract values from JSON */
 struct jsonElement *hgvsJson = jsonFindNamedField(json, "jsonData", "hgvsInput");
 if (hgvsJson)
     {
     char *hgvs = jsonStringVal(hgvsJson, "hgvsInput");
     if (hgvs)
         {
         struct hgPositions *hgp = NULL;
         AllocVar(hgp);
         hgp->query = cloneString(hgvs);
         hgp->database = database;
         hgp->useAlias = FALSE;
         if (matchesHgvs(cart, database, hgvs, hgp, measureTiming))
             {
             fixSinglePos(hgp);
             if (hgp->singlePos)
                 {
                 // fill out an item AND redirect to that location somehow?
                 item->chrom = hgp->singlePos->chrom;
                 // matchesHgvs widens the position by HGVS_FIND_PADDING on each
                 // side for visual context; the saved variant should be the exact
                 // mapped span.
                 item->chromStart = item->thickStart =
                     hgp->singlePos->chromStart + HGVS_FIND_PADDING;
                 item->chromEnd = item->thickEnd =
                     hgp->singlePos->chromEnd - HGVS_FIND_PADDING;
                 item->strand[0] = '.';
                 item->score = 0;
                 item->bin = binFromRange(item->chromStart, item->chromEnd);
                 char *hgvsRef = NULL, *hgvsAlt = NULL, *changeSeq = NULL;
                 enum hgvsChangeType changeType = hgvsctUndefined;
                 extractHgvsChange(hgvs, &hgvsRef, &hgvsAlt, &changeType, &changeSeq);
                 item->ref = hgvsRef ? hgvsRef : cloneString("");
                 item->alt = hgvsAlt ? hgvsAlt : cloneString("");
                 item->name = synthesizeItemName(item->ref, item->alt, changeType, changeSeq);
                 if (item->name == NULL)
                     item->name = cloneString("");
                 freeMem(changeSeq);
                 item->db = database;
                 item->description = cloneString(hgp->singlePos->description);
                 item->project = cloneString("");
                 item->mouseover = cloneString("");
                 item->itemType = cloneString("snv");
                 item->cnvType = cloneString("");
                 }
             }
         else
             {
             /* HGVS didn't match - try hgFind for position/gene resolution */
             struct hgPositions *hgpFind = hgPositionsFind(database, hgvs,
                 "", "myVariants", cart, FALSE, FALSE, NULL);
             if (hgpFind != NULL && hgpFind->posCount == 1)
                 {
                 struct hgPos *pos = hgpFind->singlePos;
                 item->chrom = cloneString(pos->chrom);
                 item->chromStart = item->thickStart = pos->chromStart;
                 item->chromEnd = item->thickEnd = pos->chromEnd;
                 item->strand[0] = '.';
                 item->score = 0;
                 item->bin = binFromRange(item->chromStart, item->chromEnd);
                 item->ref = cloneString("");
                 item->alt = cloneString("");
                 item->name = cloneString("");
                 item->db = database;
                 item->description = cloneString(pos->description ? pos->description : "");
                 item->project = cloneString("");
                 item->mouseover = cloneString("");
                 item->itemType = cloneString("snv");
                 item->cnvType = cloneString("");
                 }
             else if (hgpFind != NULL && hgpFind->posCount > 1)
                 {
                 warn("Position '%s' matches %d locations - please be more specific", hgvs, hgpFind->posCount);
                 return;
                 }
             else
                 {
                 warn("Position '%s' not found", hgvs);
                 return;
                 }
             }
         }
     }
 else
     {
     char *chrom = jsonStringVal(jsonFindNamedField(json, "jsonData", "chrom"), "chrom");
     int chromStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "start"), "start"));
     int chromEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "end"), "end"));
     char *name = jsonStringVal(jsonFindNamedField(json, "jsonData", "name"), "name");
     int score = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "score"), "score"));
     char *strand = jsonStringVal(jsonFindNamedField(json, "jsonData", "strand"), "strand");
     int thickStart = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickStart"), "thickStart"));
     int thickEnd = sqlUnsigned(jsonStringVal(jsonFindNamedField(json, "jsonData", "thickEnd"), "thickEnd"));
     char *colorCode = jsonStringVal(jsonFindNamedField(json, "jsonData", "color"), "color");
     char *description = jsonStringVal(jsonFindNamedField(json, "jsonData", "description"), "description");
     char *ref = jsonOptionalStringField(json, "ref", "");
     char *alt = jsonOptionalStringField(json, "alt", "");
     char *project = jsonOptionalStringField(json, "project", "");
     char *mouseover = jsonOptionalStringField(json, "mouseover", "");
     char *itemType = myVariantsCanonicalItemType(
         jsonOptionalStringField(json, "itemType", "snv"));
     if (itemType == NULL)
         errAbort("invalid itemType");
     char *cnvType = "";
     if (sameString(itemType, "cnv"))
         {
         cnvType = myVariantsCanonicalCnvType(jsonOptionalStringField(json, "cnvType", ""));
         if (cnvType == NULL)
             errAbort("invalid cnvType");
         }
     unsigned color;
     htmlColorForCode(colorCode, &color);
 
     item->bin = binFromRange(chromStart, chromEnd);
     item->chrom = cloneString(chrom);
     item->chromStart = chromStart;
     item->chromEnd = chromEnd;
     /* SNV/CNV items don't carry a thick range or BED12 blocks; force defaults
      * so a stale value from a view toggle in the client can't leak through. */
     if (sameString(itemType, "snv") || sameString(itemType, "cnv"))
         {
         item->thickStart = chromStart;
         item->thickEnd = chromEnd;
         }
     else
         {
         item->thickStart = thickStart;
         item->thickEnd = thickEnd;
         }
     if (isEmpty(name))
         {
         item->name = synthesizeItemName(ref, alt, hgvsctUndefined, NULL);
         if (item->name == NULL)
             item->name = cloneString("");
         }
     else
         item->name = cloneString(name);
     item->score = score;
     item->strand[0] = strand[0];
     item->itemRgb = color;
     item->description = cloneString(description);
     if (sameString(itemType, "transcript"))
         {
         item->ref = cloneString("");
         item->alt = cloneString("");
         }
     else if (sameString(itemType, "cnv"))
         {
         /* CNV stores the inserted/duplicated sequence in alt; ref is unused. */
         item->ref = cloneString("");
         item->alt = cloneString(alt);
         }
     else
         {
         item->ref = cloneString(ref);
         item->alt = cloneString(alt);
         }
     item->db = database;
     item->project = cloneString(project);
     item->mouseover = cloneString(mouseover);
     item->itemType = cloneString(itemType);
     item->cnvType = cloneString(cnvType);
     }
 
 if (!item)
     return;
 
 /* Parse blocks from JSON if present. Empty / missing means single full-span
  * block; ensureItemBlocks synthesizes that below. */
 struct jsonElement *blockSizesJson = jsonFindNamedField(json, "jsonData", "blockSizes");
 struct jsonElement *chromStartsJson = jsonFindNamedField(json, "jsonData", "chromStarts");
 if (blockSizesJson && blockSizesJson->type == jsonList &&
     chromStartsJson && chromStartsJson->type == jsonList)
     {
     int sizeN = slCount(blockSizesJson->val.jeList);
     int startN = slCount(chromStartsJson->val.jeList);
     if (sizeN != startN)
         errAbort("blockSizes and chromStarts lengths differ (%d vs %d)", sizeN, startN);
     if (sizeN > 0)
         {
         item->blockCount = sizeN;
         AllocArray(item->blockSizes, sizeN);
         AllocArray(item->chromStarts, sizeN);
         struct slRef *el;
         int i = 0;
         for (el = blockSizesJson->val.jeList; el != NULL; el = el->next, i++)
             {
             struct jsonElement *child = (struct jsonElement *)el->val;
             if (child->type != jsonNumber)
                 errAbort("blockSizes[%d] is not a number", i);
             item->blockSizes[i] = child->val.jeNumber;
             }
         i = 0;
         for (el = chromStartsJson->val.jeList; el != NULL; el = el->next, i++)
             {
             struct jsonElement *child = (struct jsonElement *)el->val;
             if (child->type != jsonNumber)
                 errAbort("chromStarts[%d] is not a number", i);
             item->chromStarts[i] = child->val.jeNumber;
             }
         }
     }
 ensureItemBlocks(item);
 validateItemBlocks(item);
 
 /* Parse custom fields from JSON payload and ALTER TABLE as needed */
 char *tableName = myVariantsCreateTable(userName);
 struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
 
 struct jsonElement *extraFieldsJson = jsonFindNamedField(json, "jsonData", "extraFields");
 if (extraFieldsJson && extraFieldsJson->type == jsonList)
     {
     struct slRef *el;
     for (el = extraFieldsJson->val.jeList; el != NULL; el = el->next)
         {
         struct jsonElement *cfObj = el->val;
         char *cfName = jsonOptionalStringField(cfObj, "name", NULL);
         char *cfValue = jsonOptionalStringField(cfObj, "value", "");
         if (isEmpty(cfName) || !isValidFieldName(cfName))
             continue;
         /* Add column if it doesn't already exist; un-hide if hidden */
         if (sqlFieldIndex(conn, tableName, cfName) < 0)
             {
             char hiddenName[256];
             safef(hiddenName, sizeof(hiddenName), "_hidden_%s", cfName);
             if (sqlFieldIndex(conn, tableName, hiddenName) >= 0)
                 {
                 /* Un-hide: rename _hidden_X back to X */
                 struct dyString *alterSql = sqlDyStringCreate(
                     "ALTER TABLE %s CHANGE COLUMN %s %s varchar(255) DEFAULT NULL",
                     tableName, hiddenName, cfName);
                 sqlUpdate(conn, dyStringContents(alterSql));
                 dyStringFree(&alterSql);
                 }
             else
                 {
                 struct dyString *alterSql = sqlDyStringCreate(
                     "ALTER TABLE %s ADD COLUMN %s varchar(255) DEFAULT NULL", tableName, cfName);
                 sqlUpdate(conn, dyStringContents(alterSql));
                 dyStringFree(&alterSql);
                 }
             }
         slPairAdd(&item->customFields, cfName, cloneString(cfValue));
         }
     slReverse(&item->customFields);
     }
 
 /* Add item to database. */
 myVariantsSaveToDb(conn, item, tableName, 0);
 
 /* Refresh the on-disk myVariants ctfile and point mvCtfile_<db> at it. */
 char *ctFile = myVariantsWriteCtFile(userName, database, cart);
 if (isNotEmpty(ctFile))
     {
     char varName[256];
     safef(varName, sizeof varName, MYVARIANTS_FILE_VAR_PREFIX "%s", database);
     cartSetString(cart, varName, ctFile);
     freeMem(ctFile);
     }
 
 /* Output new item coordinates for JavaScript to use for navigation */
 jsInlineF("// START newItemPos\n");
 jsInlineF("var newItemPos = {\"chrom\": \"%s\", \"start\": %d, \"end\": %d};\n",
           item->chrom, item->chromStart, item->chromEnd);
 jsInlineF("// END newItemPos\n");
 
 hFreeConn(&conn);
 
 freez(&dupeCommand);
 }
 
 static int myVariantsExtraHeight(struct track *track)
 /* Return extra height of track. */
 {
 return tl.fontHeight+2;
 }
 
 static void updateTextField(char *trackName, struct sqlConnection *conn,
 	char *tableName, char *fieldName, int id, char *scopeProject, char *scopeDb)
 /* Update text valued field with new val. If scopeProject/scopeDb are non-NULL,
  * include them in the WHERE clause so a recipient cannot edit rows outside
  * the share's authorized project/db. scopeProject of "*" means all projects. */
 {
 char varName[128];
 safef(varName, sizeof(varName), "%s_%s", trackName, fieldName);
 char *newVal = cartOptionalString(cart, varName);
 if (newVal == NULL)
     return;
 if (endsWith(varName, "itemRgb"))
     {
     unsigned color;
     if (htmlColorForCode(newVal, &color))
         {
         struct dyString *sql = sqlDyStringCreate(
             "update %s set %s='%d' where id=%d", tableName, fieldName, color, id);
         if (isNotEmpty(scopeDb))
             sqlDyStringPrintf(sql, " and db='%s'", scopeDb);
         if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*"))
             sqlDyStringPrintf(sql, " and project='%s'", scopeProject);
         sqlUpdate(conn, sql->string);
         dyStringFree(&sql);
         cartRemove(cart, varName);
         }
     return;
     }
 if (sameString(fieldName, "cnvType"))
     {
     if (isNotEmpty(newVal) && myVariantsCanonicalCnvType(newVal) == NULL)
         errAbort("invalid cnvType");
     newVal = isEmpty(newVal) ? "" : myVariantsCanonicalCnvType(newVal);
     }
 struct dyString *sql = sqlDyStringCreate("update %s set %s='%s' where id=%d",
     tableName, fieldName, newVal, id);
 if (isNotEmpty(scopeDb))
     sqlDyStringPrintf(sql, " and db='%s'", scopeDb);
 if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*"))
     sqlDyStringPrintf(sql, " and project='%s'", scopeProject);
 sqlUpdate(conn, sql->string);
 dyStringFree(&sql);
 cartRemove(cart, varName);
 }
 
 static void updateBlocksFields(char *trackName, struct sqlConnection *conn,
         char *tableName, int id, char *scopeProject, char *scopeDb)
 /* Pull <trackName>_blockCount, _blockSizes, _chromStarts from the cart,
  * validate jointly against the row's current chromStart/chromEnd via
  * loadAndValidateBed, and UPDATE all three columns in one statement.
  * No-op if any of the three cart vars are missing. */
 {
 char vC[128], vS[128], vT[128];
 safef(vC, sizeof vC, "%s_blockCount", trackName);
 safef(vS, sizeof vS, "%s_blockSizes", trackName);
 safef(vT, sizeof vT, "%s_chromStarts", trackName);
 char *cartCount = cartOptionalString(cart, vC);
 char *cartSizes = cartOptionalString(cart, vS);
 char *cartStarts = cartOptionalString(cart, vT);
 if (cartCount == NULL || cartSizes == NULL || cartStarts == NULL)
     return;
 /* Clone the cart strings before removing the cart vars: if validation
  * errAborts below, the cart vars must not survive to corrupt the next
  * edit on a different row. */
 char *blockCountStr = cloneString(cartCount);
 char *blockSizesStr = cloneString(cartSizes);
 char *chromStartsStr = cloneString(cartStarts);
 cartRemove(cart, vC);
 cartRemove(cart, vS);
 cartRemove(cart, vT);
 
 struct dyString *q = sqlDyStringCreate(
     "select chrom,chromStart,chromEnd,name,score,strand,thickStart,thickEnd,itemRgb"
     " from %s where id=%d", tableName, id);
 if (isNotEmpty(scopeDb))
     sqlDyStringPrintf(q, " and db='%s'", scopeDb);
 if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*"))
     sqlDyStringPrintf(q, " and project='%s'", scopeProject);
 struct sqlResult *sr = sqlGetResult(conn, q->string);
 dyStringFree(&q);
 char **row = sqlNextRow(sr);
 if (row == NULL)
     {
     sqlFreeResult(&sr);
     freeMem(blockCountStr);
     freeMem(blockSizesStr);
     freeMem(chromStartsStr);
     return;
     }
 struct myVariants item;
 ZeroVar(&item);
 item.chrom = cloneString(row[0]);
 item.chromStart = sqlUnsigned(row[1]);
 item.chromEnd = sqlUnsigned(row[2]);
 item.name = cloneString(row[3]);
 item.score = sqlUnsigned(row[4]);
 safecpy(item.strand, sizeof(item.strand), row[5]);
 item.thickStart = sqlUnsigned(row[6]);
 item.thickEnd = sqlUnsigned(row[7]);
 item.itemRgb = sqlUnsigned(row[8]);
 sqlFreeResult(&sr);
 
 item.blockCount = sqlUnsigned(blockCountStr);
 char *sizesDup = cloneString(blockSizesStr);
 char *startsDup = cloneString(chromStartsStr);
 int n;
 sqlSignedDynamicArray(sizesDup, &item.blockSizes, &n);
 if (n != item.blockCount)
     errAbort("blockSizes count %d != blockCount %u", n, item.blockCount);
 sqlSignedDynamicArray(startsDup, &item.chromStarts, &n);
 if (n != item.blockCount)
     errAbort("chromStarts count %d != blockCount %u", n, item.blockCount);
 freeMem(sizesDup);
 freeMem(startsDup);
 
 validateItemBlocks(&item);
 
 struct dyString *upd = sqlDyStringCreate(
     "update %s set blockCount=%u,blockSizes='%s',chromStarts='%s' where id=%d",
     tableName, item.blockCount, blockSizesStr, chromStartsStr, id);
 if (isNotEmpty(scopeDb))
     sqlDyStringPrintf(upd, " and db='%s'", scopeDb);
 if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*"))
     sqlDyStringPrintf(upd, " and project='%s'", scopeProject);
 sqlUpdate(conn, upd->string);
 dyStringFree(&upd);
 
 freeMem(item.chrom);
 freeMem(item.name);
 freeMem(item.blockSizes);
 freeMem(item.chromStarts);
 freeMem(blockCountStr);
 freeMem(blockSizesStr);
 freeMem(chromStartsStr);
 }
 
 static void myVariantsEditOrDelete(char *trackName, struct sqlConnection *conn, char *tableName)
 /* Troll through cart variables looking for things that indicate user edited item
  * or deleted it in hgc,  and carry out edits. See hgc/myVariantsClick.c. */
 {
 char varName[128];
 char sql[256];
 safef(varName, sizeof(varName), "%s_%s", trackName, "id");
 char *idString = cartOptionalString(cart, varName);
 if (idString != NULL)
     {
     int id = sqlUnsigned(idString);
     idString = NULL; // Will be no good after cartRemove
     cartRemove(cart, varName); // Remove so only do edits once.
 
     /* Handle cancel. */
     safef(varName, sizeof(varName), "%s_%s", trackName, "cancel");
     if (cartVarExists(cart, varName))
         {
         cartRemove(cart, varName); // Only want to do cancels once
         return;
         }
 
     /* Handle delete. */
     safef(varName, sizeof(varName), "%s_%s", trackName, "delete");
     if (cartVarExists(cart, varName))
         {
         cartRemove(cart, varName);	// Especially only want to do deletes once!
         sqlSafef(sql, sizeof(sql), "delete from %s where id=%d", tableName, id);
         sqlUpdate(conn, sql);
         /* Refresh the on-disk myVariants ctfile after a delete. */
         char *userName = getUserName();
         if (userName)
             {
             char *ctFile = myVariantsWriteCtFile(userName, database, cart);
             if (isNotEmpty(ctFile))
                 {
                 char ctVar[256];
                 safef(ctVar, sizeof ctVar, MYVARIANTS_FILE_VAR_PREFIX "%s", database);
                 cartSetString(cart, ctVar, ctFile);
                 freeMem(ctFile);
                 }
             }
         return;
         }
 
     /* Handle edits. Owner edits their own table, no project/db scope filter. */
     updateTextField(trackName, conn, tableName, "name", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "description", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "itemRgb", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "chromStart", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "chromEnd", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "thickStart", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "thickEnd", id, NULL, NULL);
     updateBlocksFields(trackName, conn, tableName, id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "ref", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "alt", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "project", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "mouseover", id, NULL, NULL);
     updateTextField(trackName, conn, tableName, "cnvType", id, NULL, NULL);
     /* Update any custom fields */
         {
         char *editUserName = getUserName();
         struct slName *customCols = myVariantsGetCustomFields(editUserName);
         struct slName *col;
         for (col = customCols; col != NULL; col = col->next)
             updateTextField(trackName, conn, tableName, col->name, id, NULL, NULL);
         slFreeList(&customCols);
         }
     /* Refresh the on-disk myVariants ctfile after edits. */
     {
         char *userName = getUserName();
         if (userName)
             {
             char *ctFile = myVariantsWriteCtFile(userName, database, cart);
             if (isNotEmpty(ctFile))
                 {
                 char ctVar[256];
                 safef(ctVar, sizeof ctVar, MYVARIANTS_FILE_VAR_PREFIX "%s", database);
                 cartSetString(cart, ctVar, ctFile);
                 freeMem(ctFile);
                 }
             }
     }
     }
 }
 
 static struct linkedFeatures *loadMyVariantsItems(struct sqlConnection *conn,
     char *tableName, struct trackDb *tdb, char *whereExtra)
 /* Query tableName for myVariants items in the current window matching the
  * whereExtra clause (e.g. "db='hg38' and project='test'") and return them
  * as a linkedFeatures list. */
 {
 struct linkedFeatures *lf, *lfList = NULL;
 struct dyString *query = sqlDyStringCreate("select * from %s where ", tableName);
 hAddBinToQueryGeneral("bin", winStart, winEnd, query);
 sqlDyStringPrintf(query, " chrom='%s' and chromStart < %d and chromEnd > %d",
         chromName, winEnd, winStart);
 if (isNotEmpty(whereExtra))
     sqlDyStringPrintf(query, " and (%-s)", whereExtra);
 struct sqlResult *sr = sqlGetResult(conn, query->string);
 dyStringFree(&query);
 char **row;
 struct dyString *mouseover = dyStringNew(0);
 while ((row = sqlNextRow(sr)) != NULL)
     {
     struct myVariants *item = myVariantsLoad(row);
     struct bed *bed;
     AllocVar(bed);
     /* bed->name is the item identifier passed to hgc, which parses "id name"
      * to look up the row by primary key.  myVariantsName() strips the "id "
      * prefix for the displayed label. */
     char buf[64];
     safef(buf, sizeof(buf), "%u %s", item->id, item->name);
     bed->chrom = item->chrom;
     bed->chromStart = item->chromStart;
     bed->chromEnd = item->chromEnd;
     bed->name = cloneString(buf);
     bed->score = item->score;
     bed->strand[0] = item->strand[0];
     bed->thickStart = item->thickStart;
     bed->thickEnd = item->thickEnd;
     bed->itemRgb = item->itemRgb;
     bed->blockCount = item->blockCount;
     if (item->blockCount > 0)
         {
         bed->blockSizes = cloneMem(item->blockSizes,
                                    item->blockCount * sizeof(int));
         bed->chromStarts = cloneMem(item->chromStarts,
                                     item->blockCount * sizeof(int));
         }
     lf = bedMungToLinkedFeatures(&bed, tdb, 12, 0, 1000, TRUE);
     dyStringClear(mouseover);
     if (item->mouseover && isNotEmpty(item->mouseover))
         lf->mouseOver = cloneString(item->mouseover);
     else
         {
         dyStringPrintf(mouseover, "%s", item->name);
         if (item->ref != NULL && isNotEmpty(item->ref))
             dyStringPrintf(mouseover, "<br>Ref: %s", item->ref);
         if (item->alt != NULL && isNotEmpty(item->alt))
             dyStringPrintf(mouseover, "<br>Alt: %s", item->alt);
         lf->mouseOver = cloneString(dyStringContents(mouseover));
         }
     slAddHead(&lfList, lf);
     }
 sqlFreeResult(&sr);
 dyStringFree(&mouseover);
 return lfList;
 }
 
 void myVariantsLoadItems(struct track *track)
 /* Load up items in track already.  Also make up a pseudo-item that is
  * where you drag to create an item. */
 {
 struct linkedFeatures *lfList = NULL;
 boolean isShared = isMyVariantsSharedTrack(track->track);
 
 if (isShared)
     {
     /* Resolve via hgcentral so revoked/downgraded shares stop returning data. */
     struct myVariantsShare *share = myVariantsResolveSharedTrack(track->track, cart);
     if (share == NULL)
         return;
     /* Shared tracks are per-assembly; only load when the browser's current
      * db matches the share's db. */
     if (!sameString(share->db, database))
         {
         myVariantsShareFree(&share);
         return;
         }
     char *tableName = myVariantsGetDbTable(share->ownerUser);
     if (isEmpty(tableName))
         {
         myVariantsShareFree(&share);
         return;
         }
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     struct dyString *whereClause = sqlDyStringCreate("db='%s'", share->db);
     if (!sameString(share->project, "*"))
         sqlDyStringPrintf(whereClause, " and project='%s'", share->project);
     lfList = loadMyVariantsItems(conn, tableName, track->tdb,
         dyStringCannibalize(&whereClause));
     hFreeConn(&conn);
     myVariantsShareFree(&share);
     }
 else
     {
     /* Load user's own items */
     char *userName = getUserName();
     char *db = myVariantsGetDatabaseForUser(userName);
     if (!db)
         return;
     char *tableName = myVariantsGetDbTable(userName);
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     myVariantsEditOrDelete(track->track, conn, tableName);
     struct dyString *whereClause = sqlDyStringCreate("db='%s'", database);
     lfList = loadMyVariantsItems(conn, tableName, track->tdb,
         dyStringCannibalize(&whereClause));
     hFreeConn(&conn);
     }
 slReverse(&lfList);
 track->items = lfList;
 }
 
 char *myVariantsName(struct track *track, void *item)
 /* Return the display label, stripping the "id " prefix that lf->name carries
  * for hgc's lookup. */
 {
 struct linkedFeatures *lf = item;
 char *name = lf->name;
 if (name == NULL)
     return name;
 char *space = strchr(name, ' ');
 if (space != NULL)
     return space + 1;
 return name;
 }
 
 void myVariantsDrawLeftLabels(struct track *track, int seqStart, int seqEnd,
     struct hvGfx *hvg, int xOff, int yOff, int width, int height,
     boolean withCenterLabels, MgFont *font,
     Color color, enum trackVisibility vis)
 /* Draw left label - just in dense or full mode. Needed to cope with empty space at top of track. */
 {
 int y = yOff + myVariantsExtraHeight(track);
 int fontHeight = mgFontLineHeight(font);
 if (withCenterLabels)
     y += mgFontLineHeight(font);
 if (vis == tvDense)
     hvGfxTextRight(hvg, xOff, y, width-1, fontHeight, color, font, track->shortLabel);
 else if (vis == tvFull)
     {
     struct bed *bed, *bedList = track->items;
     for (bed = bedList; bed != NULL; bed = bed->next)
 	{
 	if (track->itemLabelColor != NULL)
 	    color = track->itemLabelColor(track, bed, hvg);
 	int itemHeight = track->itemHeight(track, bed);
 	hvGfxTextRight(hvg, xOff, y, width - 1,
 	    itemHeight, color, font, track->itemName(track, bed));
 	y += itemHeight;
 	}
     }
 /* In pack mode the draw routine takes care of the labels. */
 }
 
 
 int myVariantsTotalHeight(struct track *track, enum trackVisibility vis)
 /* Most fixed height track groups will use this to figure out the height
  * they use. */
 {
 track->height = tgFixedTotalHeightOptionalOverflow(track, vis, tl.fontHeight+1, tl.fontHeight, FALSE) + 
 	myVariantsExtraHeight(track);
 return track->height;
 }
 
 void myVariantsMethods(struct track *track)
 /* Set up special methods for myVariants type tracks. */
 {
 linkedFeaturesMethods(track);
 track->totalHeight = myVariantsTotalHeight;
 track->drawLeftLabels = myVariantsDrawLeftLabels;
 track->loadItems = myVariantsLoadItems;
 track->itemName = myVariantsName;
 track->nextItemButtonable = TRUE;
 }
 
 static void apiSuccess(struct jsonWrite *jw)
 /* Send 200 JSON response and exit. */
 {
 printf("Content-Type: application/json\n\n");
 puts(jw->dy->string);
 jsonWriteFree(&jw);
 exit(0);
 }
 
 static void apiError(int httpStatus, char *message)
 /* Send error JSON response with given HTTP status and exit. */
 {
 printf("Status: %d\n", httpStatus);
 printf("Content-Type: application/json\n\n");
 struct jsonWrite *jw = jsonWriteNew();
 jsonWriteObjectStart(jw, NULL);
 jsonWriteString(jw, "error", message);
 jsonWriteObjectEnd(jw);
 puts(jw->dy->string);
 jsonWriteFree(&jw);
 exit(0);
 }
 
 static void shareToJson(struct jsonWrite *jw, struct myVariantsShare *share)
 /* Write a single share record as a JSON object into jw. */
 {
 jsonWriteObjectStart(jw, NULL);
 jsonWriteNumber(jw, "id", share->id);
 jsonWriteString(jw, "ownerUser", share->ownerUser);
 jsonWriteString(jw, "shareToken", share->shareToken);
 jsonWriteString(jw, "project", share->project);
 jsonWriteString(jw, "db", share->db);
 jsonWriteNumber(jw, "permission", share->permission);
 jsonWriteString(jw, "targetUser", share->targetUser);
 jsonWriteString(jw, "label", share->label);
 jsonWriteString(jw, "createdAt", share->createdAt);
 jsonWriteObjectEnd(jw);
 }
 
+static char *cleanShareTargets(struct sqlConnection *conn, char *raw)
+/* Parse a comma-separated recipient string into a normalized comma-no-space
+ * list: trim each name, drop empties, de-dup, and verify each exists in
+ * gbMembers. Calls apiError(400,...) (does not return) on an unknown name or
+ * an overlong list. Returns a string the caller must freeMem, or NULL for an
+ * empty list (anyone with link). */
+{
+if (isEmpty(raw))
+    return NULL;
+struct slName *names = slNameListFromComma(raw);
+struct slName *name;
+struct slName *cleanList = NULL;
+for (name = names; name != NULL; name = name->next)
+    {
+    trimSpaces(name->name);
+    if (isNotEmpty(name->name))
+        slNameStore(&cleanList, name->name);
+    }
+slNameFreeList(&names);
+if (cleanList == NULL)
+    return NULL;
+slReverse(&cleanList);  /* slNameStore prepends; restore input order */
+
+struct dyString *dy = dyStringNew(256);
+struct slName *c;
+for (c = cleanList; c != NULL; c = c->next)
+    {
+    char gbq[512];
+    sqlSafef(gbq, sizeof(gbq),
+        "select count(*) from gbMembers where userName = BINARY '%s'", c->name);
+    if (sqlQuickNum(conn, gbq) == 0)
+        {
+        char msg[512];
+        safef(msg, sizeof(msg),
+            "user '%s' not found (the username is case-sensitive) - check the"
+            " spelling, or leave the field blank to make an 'anyone with link' share",
+            c->name);
+        apiError(400, msg);
+        }
+    if (dy->stringSize > 0)
+        dyStringAppendC(dy, ',');
+    dyStringAppend(dy, c->name);
+    }
+slNameFreeList(&cleanList);
+if (dy->stringSize > 500)
+    {
+    dyStringFree(&dy);
+    apiError(400, "too many recipients - the username list is too long");
+    }
+return dyStringCannibalize(&dy);
+}
+
 static boolean isStateChangingShareAction(char *action)
 /* Returns TRUE for actions that modify the share table.  Those require an
  * hgsid match to defend against CSRF; read-only actions don't, because the
  * response only goes to the originating cookie-bearing browser. */
 {
-return sameString(action, "createShare") || sameString(action, "revokeShare");
+return sameString(action, "createShare") || sameString(action, "revokeShare")
+    || sameString(action, "setSharePermission") || sameString(action, "setShareTargets");
 }
 
 void myVariantsShareApiHandler(char *action)
 /* Handle share API requests. Outputs JSON to stdout and calls exit(0).
  * Called from main() before cartHtmlShell. */
 {
 char *userName = getUserName();
 if (userName == NULL)
     apiError(401, "not logged in");
 
 /* CSRF protection for state-changing actions: require the request to carry
  * an hgsid matching the user's session.  An attacker page can fire requests
  * with the user's cookies but cannot read or guess the session id, so this
  * blocks cross-site forgery of share creation/revocation. */
 if (isStateChangingShareAction(action))
     {
     char *suppliedHgsid = cgiOptionalString("hgsid");
     char *expectedHgsid = cartSessionId(cart);
     if (isEmpty(suppliedHgsid) || isEmpty(expectedHgsid)
         || !sameString(suppliedHgsid, expectedHgsid))
         apiError(403, "session token missing or invalid");
     }
 
 char *db = cgiOptionalString("db");
 if (db == NULL)
     apiError(400, "missing required parameter: db");
 
 struct sqlConnection *conn = hConnectCentral();
 
 if (!sqlTableExists(conn, "myVariantsShares"))
     {
     hDisconnectCentral(&conn);
     apiError(500, "myVariantsShares table not found - contact genome-www@ucsc.edu");
     }
 
 if (sameString(action, "createShare"))
     {
     char *project = cgiOptionalString("project");
     if (project == NULL)
         apiError(400, "missing required parameter: project");
     int permission = cgiOptionalInt("permission", MYVAR_PERM_READONLY);
     if (permission != MYVAR_PERM_READONLY && permission != MYVAR_PERM_READWRITE)
         apiError(400, "permission must be 0 or 1");
     char *targetUser = cgiOptionalString("targetUser");
     char *label = cgiOptionalString("label");
-    /* Reject overlong fields before MySQL would (varchar(255)).  Use 200 to
-     * leave room for any prefixing/quoting we add downstream. */
-    if (strlen(project) > 200
-        || (targetUser && strlen(targetUser) > 200)
-        || (label && strlen(label) > 200))
-        apiError(400, "project, targetUser, and label must each be <= 200 chars");
+    /* Reject overlong fields before MySQL would.  Use 200 to leave room for any
+     * prefixing/quoting we add downstream; the recipient list is checked in
+     * cleanShareTargets against the wider targetUser column. */
+    if (strlen(project) > 200 || (label && strlen(label) > 200))
+        apiError(400, "project and label must each be <= 200 chars");
     /* Project must be "*" (all) or one of the owner's existing project values.
      * Prevents creating misleading share URLs that filter on a non-existent
      * project name. */
     if (!sameString(project, "*"))
         {
         struct slName *projects = myVariantsGetProjects(userName);
         boolean found = slNameInList(projects, project);
         slFreeList(&projects);
         if (!found)
             apiError(400, "project does not exist for current user");
         }
-    if (isNotEmpty(targetUser))
-        {
-        char gbq[512];
-        sqlSafef(gbq, sizeof(gbq),
-            "select count(*) from gbMembers where userName='%s'", targetUser);
-        if (sqlQuickNum(conn, gbq) == 0)
-            apiError(400, "targetUser not found - check the spelling, or omit"
-                " targetUser to make an 'anyone with link' share");
-        }
+    char *cleanTargets = cleanShareTargets(conn, targetUser);
     struct myVariantsShare *share = myVariantsCreateShare(conn, userName,
-        project, db, permission, targetUser, label);
+        project, db, permission, cleanTargets, label);
+    freeMem(cleanTargets);
     struct jsonWrite *jw = jsonWriteNew();
     jsonWriteObjectStart(jw, NULL);
     jsonWriteString(jw, "status", "ok");
     jsonWriteString(jw, "token", share->shareToken);
     jsonWriteNumber(jw, "id", share->id);
     jsonWriteObjectEnd(jw);
     myVariantsShareFree(&share);
     hDisconnectCentral(&conn);
     apiSuccess(jw);
     }
 else if (sameString(action, "getShares"))
     {
     struct myVariantsShare *shares = myVariantsGetSharesForOwner(conn, userName, db);
     struct jsonWrite *jw = jsonWriteNew();
     jsonWriteObjectStart(jw, NULL);
     jsonWriteString(jw, "status", "ok");
     jsonWriteListStart(jw, "shares");
     struct myVariantsShare *share;
     for (share = shares; share != NULL; share = share->next)
         shareToJson(jw, share);
     jsonWriteListEnd(jw);
     jsonWriteObjectEnd(jw);
     myVariantsShareFreeList(&shares);
     hDisconnectCentral(&conn);
     apiSuccess(jw);
     }
 else if (sameString(action, "getSharesForMe"))
     {
     struct myVariantsShare *shares = myVariantsGetSharesForUser(conn, userName, db);
     struct jsonWrite *jw = jsonWriteNew();
     jsonWriteObjectStart(jw, NULL);
     jsonWriteString(jw, "status", "ok");
     jsonWriteListStart(jw, "shares");
     struct myVariantsShare *share;
     for (share = shares; share != NULL; share = share->next)
         shareToJson(jw, share);
     jsonWriteListEnd(jw);
     jsonWriteObjectEnd(jw);
     myVariantsShareFreeList(&shares);
     hDisconnectCentral(&conn);
     apiSuccess(jw);
     }
 else if (sameString(action, "revokeShare"))
     {
     char *token = cgiOptionalString("shareToken");
     if (token == NULL)
         apiError(400, "missing required parameter: shareToken");
     boolean revoked = myVariantsRevokeShare(conn, token, userName);
     hDisconnectCentral(&conn);
     if (!revoked)
         apiError(404, "share not found");
     struct jsonWrite *jw = jsonWriteNew();
     jsonWriteObjectStart(jw, NULL);
     jsonWriteString(jw, "status", "ok");
     jsonWriteObjectEnd(jw);
     apiSuccess(jw);
     }
+else if (sameString(action, "setSharePermission"))
+    {
+    char *token = cgiOptionalString("shareToken");
+    if (token == NULL)
+        apiError(400, "missing required parameter: shareToken");
+    int permission = cgiOptionalInt("permission", MYVAR_PERM_READONLY);
+    if (permission != MYVAR_PERM_READONLY && permission != MYVAR_PERM_READWRITE)
+        apiError(400, "permission must be 0 or 1");
+    boolean updated = myVariantsSetSharePermission(conn, token, userName, permission);
+    hDisconnectCentral(&conn);
+    if (!updated)
+        apiError(404, "share not found");
+    struct jsonWrite *jw = jsonWriteNew();
+    jsonWriteObjectStart(jw, NULL);
+    jsonWriteString(jw, "status", "ok");
+    jsonWriteObjectEnd(jw);
+    apiSuccess(jw);
+    }
+else if (sameString(action, "setShareTargets"))
+    {
+    char *token = cgiOptionalString("shareToken");
+    if (token == NULL)
+        apiError(400, "missing required parameter: shareToken");
+    char *cleanTargets = cleanShareTargets(conn, cgiOptionalString("targetUser"));
+    boolean updated = myVariantsSetShareTargets(conn, token, userName, cleanTargets);
+    freeMem(cleanTargets);
+    hDisconnectCentral(&conn);
+    if (!updated)
+        apiError(404, "share not found");
+    struct jsonWrite *jw = jsonWriteNew();
+    jsonWriteObjectStart(jw, NULL);
+    jsonWriteString(jw, "status", "ok");
+    jsonWriteObjectEnd(jw);
+    apiSuccess(jw);
+    }
 else
     {
     hDisconnectCentral(&conn);
     apiError(400, "unknown action");
     }
 }
 
 void myVariantsProcessShareParam()
 /* Check for myVarShare CGI param, validate the share token, and store
  * the share reference in the cart. Must be called before track loading. */
 {
 char *token = cgiOptionalString(MYVAR_SHARE_CGI_VAR);
 if (isEmpty(token))
     return;
 
 if (strlen(token) != MYVAR_TOKEN_LENGTH)
     {
     notify("A share link in the URL couldn't be read. The shared track hasn't been added, but the rest of the browser works normally.", "myVarShareInvalidToken");
     return;
     }
 
 char *userName = getUserName();
 
 struct sqlConnection *conn = hConnectCentral();
 if (!sqlTableExists(conn, "myVariantsShares"))
     {
     hDisconnectCentral(&conn);
     return;
     }
 
 struct myVariantsShare *share = myVariantsGetShareByToken(conn, token);
 hDisconnectCentral(&conn);
 if (share == NULL)
     {
     notify("A share link is no longer valid (it may have been revoked). The shared track isn't available, but the rest of the browser works normally.", "myVarShareRevoked");
     return;
     }
 
-/* Targeted shares require the matching logged-in user. Shares with no targetUser
- * ("anyone with link") are accessible to anon users too. */
+/* Targeted shares require a logged-in user in the share's recipient list.
+ * Shares with no targetUser ("anyone with link") are accessible to anon users too. */
 if (isNotEmpty(share->targetUser))
     {
     if (userName == NULL)
         {
         notify("A shared track in this view is limited to a specific user. You can keep browsing.", "myVarShareNeedLogin");
         myVariantsShareFree(&share);
         return;
         }
-    if (!sameString(share->targetUser, userName))
+    if (!myVariantsShareAllowsUser(share, userName))
         {
         notify("A shared track in this view wasn't shared with your account. You can keep browsing; the track just won't appear.", "myVarShareWrongUser");
         myVariantsShareFree(&share);
         return;
         }
     }
 
 /* Store in cart so the shared track loader can find it during track loading */
 char cartVar[256];
 safef(cartVar, sizeof(cartVar), MYVAR_SHARED_CART_PREFIX "%s", token);
 char *cartVal = myVariantsShareCartValue(share);
 cartSetString(cart, cartVar, cartVal);
 freeMem(cartVal);
 myVariantsShareFree(&share);
 }
 
 void myVariantsProcessSharedEdits()
 /* Process pending edits for shared tracks before any track loading.
  * This ensures edits are committed to the database before both the
  * owner's track and the shared track query for items. */
 {
 /* Anon users cannot edit shared items, regardless of share permission. */
 char *userName = getUserName();
 if (userName == NULL)
     return;
 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);
 
     /* Check if there's a pending edit for this shared track */
     char varName[256];
     safef(varName, sizeof(varName), "%s_%s", trackName, "id");
     char *idString = cartOptionalString(cart, varName);
     if (idString == NULL)
         continue;
 
     /* Revalidate via hgcentral so revoked/downgraded shares can't write. */
     struct myVariantsShare *liveShare = myVariantsResolveSharedTrack(trackName, cart);
     if (liveShare == NULL)
         continue;
     if (liveShare->permission != MYVAR_PERM_READWRITE)
         {
         myVariantsShareFree(&liveShare);
         continue;
         }
     char *tableName = myVariantsGetDbTable(liveShare->ownerUser);
     if (isEmpty(tableName))
         {
         myVariantsShareFree(&liveShare);
         continue;
         }
 
     int id = sqlUnsigned(idString);
     cartRemove(cart, varName);
 
     /* Handle cancel */
     safef(varName, sizeof(varName), "%s_%s", trackName, "cancel");
     if (cartVarExists(cart, varName))
         {
         cartRemove(cart, varName);
         myVariantsShareFree(&liveShare);
         continue;
         }
 
     /* Apply field edits, scoped to the share's project/db so a recipient
      * cannot edit rows in projects/assemblies they were not granted. */
     char *sp = liveShare->project;
     char *sd = liveShare->db;
     struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH);
     updateTextField(trackName, conn, tableName, "name", id, sp, sd);
     updateTextField(trackName, conn, tableName, "description", id, sp, sd);
     updateTextField(trackName, conn, tableName, "itemRgb", id, sp, sd);
     updateTextField(trackName, conn, tableName, "chromStart", id, sp, sd);
     updateTextField(trackName, conn, tableName, "chromEnd", id, sp, sd);
     updateTextField(trackName, conn, tableName, "thickStart", id, sp, sd);
     updateTextField(trackName, conn, tableName, "thickEnd", id, sp, sd);
     updateBlocksFields(trackName, conn, tableName, id, sp, sd);
     updateTextField(trackName, conn, tableName, "ref", id, sp, sd);
     updateTextField(trackName, conn, tableName, "alt", id, sp, sd);
     updateTextField(trackName, conn, tableName, "mouseover", id, sp, sd);
     updateTextField(trackName, conn, tableName, "cnvType", id, sp, sd);
     struct slName *customCols = myVariantsGetCustomFields(liveShare->ownerUser);
     struct slName *col;
     for (col = customCols; col != NULL; col = col->next)
         updateTextField(trackName, conn, tableName, col->name, id, sp, sd);
     slFreeList(&customCols);
     hFreeConn(&conn);
     myVariantsShareFree(&liveShare);
     }
 hashElFreeList(&shareVars);
 }
 
 boolean myVariantsTrackEnabled()
 /* Return TRUE if the "My Variants" feature is enabled in hg.conf
  * and the current request comes from a valid user */
 {
 return cfgOptionBooleanDefault("doMyVariants", FALSE) && (getUserName() != NULL);
 }