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/myVariantsShare.c src/hg/lib/myVariantsShare.c
index eedf1f82fc0..ab34ab6fb26 100644
--- src/hg/lib/myVariantsShare.c
+++ src/hg/lib/myVariantsShare.c
@@ -1,329 +1,405 @@
 /* myVariantsShare.c was originally generated by the autoSql program, which also 
  * generated myVariantsShare.h and myVariantsShare.sql.  This module links the database and
  * the RAM representation of objects. */
 
 #include "common.h"
 #include "linefile.h"
 #include "dystring.h"
 #include "jksql.h"
 #include "myVariantsShare.h"
 #include "htmshell.h"
 #include "jsonParse.h"
 #include "jsonWrite.h"
 #include "errCatch.h"
 
 
 
 char *myVariantsShareCommaSepFieldNames = "id,ownerUser,shareToken,project,db,permission,targetUser,label,createdAt";
 
 void myVariantsShareStaticLoad(char **row, struct myVariantsShare *ret)
 /* Load a row from myVariantsShare table into ret.  The contents of ret will
  * be replaced at the next call to this function. */
 {
 
 ret->id = sqlUnsigned(row[0]);
 ret->ownerUser = row[1];
 ret->shareToken = row[2];
 ret->project = row[3];
 ret->db = row[4];
 ret->permission = sqlUnsigned(row[5]);
 ret->targetUser = row[6];
 ret->label = row[7];
 ret->createdAt = row[8];
 }
 
 struct myVariantsShare *myVariantsShareLoadByQuery(struct sqlConnection *conn, char *query)
 /* Load all myVariantsShare from table that satisfy the query given.  
  * Where query is of the form 'select * from example where something=something'
  * or 'select example.* from example, anotherTable where example.something = 
  * anotherTable.something'.
  * Dispose of this with myVariantsShareFreeList(). */
 {
 struct myVariantsShare *list = NULL, *el;
 struct sqlResult *sr;
 char **row;
 
 sr = sqlGetResult(conn, query);
 while ((row = sqlNextRow(sr)) != NULL)
     {
     el = myVariantsShareLoad(row);
     slAddHead(&list, el);
     }
 slReverse(&list);
 sqlFreeResult(&sr);
 return list;
 }
 
 void myVariantsShareSaveToDb(struct sqlConnection *conn, struct myVariantsShare *el, char *tableName, int updateSize)
 /* Save myVariantsShare as a row to the table specified by tableName. 
  * As blob fields may be arbitrary size updateSize specifies the approx size
  * of a string that would contain the entire query. Arrays of native types are
  * converted to comma separated strings and loaded as such, User defined types are
  * inserted as NULL. This function automatically escapes quoted strings for mysql. */
 {
 struct dyString *update = dyStringNew(updateSize);
 sqlDyStringPrintf(update, "insert into %s values ( %u,'%s','%s','%s','%s',%u,'%s','%s','%s')", 
 	tableName,  el->id,  el->ownerUser,  el->shareToken,  el->project,  el->db,  el->permission,  el->targetUser,  el->label,  el->createdAt);
 sqlUpdate(conn, update->string);
 dyStringFree(&update);
 }
 
 struct myVariantsShare *myVariantsShareLoad(char **row)
 /* Load a myVariantsShare from row fetched with select * from myVariantsShare
  * from database.  Dispose of this with myVariantsShareFree(). */
 {
 struct myVariantsShare *ret;
 
 AllocVar(ret);
 ret->id = sqlUnsigned(row[0]);
 ret->ownerUser = cloneString(row[1]);
 ret->shareToken = cloneString(row[2]);
 ret->project = cloneString(row[3]);
 ret->db = cloneString(row[4]);
 ret->permission = sqlUnsigned(row[5]);
 ret->targetUser = cloneString(row[6]);
 ret->label = cloneString(row[7]);
 ret->createdAt = cloneString(row[8]);
 return ret;
 }
 
 struct myVariantsShare *myVariantsShareLoadAll(char *fileName) 
 /* Load all myVariantsShare from a whitespace-separated file.
  * Dispose of this with myVariantsShareFreeList(). */
 {
 struct myVariantsShare *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
 char *row[9];
 
 while (lineFileRow(lf, row))
     {
     el = myVariantsShareLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct myVariantsShare *myVariantsShareLoadAllByChar(char *fileName, char chopper) 
 /* Load all myVariantsShare from a chopper separated file.
  * Dispose of this with myVariantsShareFreeList(). */
 {
 struct myVariantsShare *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
 char *row[9];
 
 while (lineFileNextCharRow(lf, chopper, row, ArraySize(row)))
     {
     el = myVariantsShareLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct myVariantsShare *myVariantsShareCommaIn(char **pS, struct myVariantsShare *ret)
 /* Create a myVariantsShare out of a comma separated string. 
  * This will fill in ret if non-null, otherwise will
  * return a new myVariantsShare */
 {
 char *s = *pS;
 
 if (ret == NULL)
     AllocVar(ret);
 ret->id = sqlUnsignedComma(&s);
 ret->ownerUser = sqlStringComma(&s);
 ret->shareToken = sqlStringComma(&s);
 ret->project = sqlStringComma(&s);
 ret->db = sqlStringComma(&s);
 ret->permission = sqlUnsignedComma(&s);
 ret->targetUser = sqlStringComma(&s);
 ret->label = sqlStringComma(&s);
 ret->createdAt = sqlStringComma(&s);
 *pS = s;
 return ret;
 }
 
 void myVariantsShareFree(struct myVariantsShare **pEl)
 /* Free a single dynamically allocated myVariantsShare such as created
  * with myVariantsShareLoad(). */
 {
 struct myVariantsShare *el;
 
 if ((el = *pEl) == NULL) return;
 freeMem(el->ownerUser);
 freeMem(el->shareToken);
 freeMem(el->project);
 freeMem(el->db);
 freeMem(el->targetUser);
 freeMem(el->label);
 freeMem(el->createdAt);
 freez(pEl);
 }
 
 void myVariantsShareFreeList(struct myVariantsShare **pList)
 /* Free a list of dynamically allocated myVariantsShare's */
 {
 struct myVariantsShare *el, *next;
 
 for (el = *pList; el != NULL; el = next)
     {
     next = el->next;
     myVariantsShareFree(&el);
     }
 *pList = NULL;
 }
 
 void myVariantsShareOutput(struct myVariantsShare *el, FILE *f, char sep, char lastSep) 
 /* Print out myVariantsShare.  Separate fields with sep. Follow last field with lastSep. */
 {
 fprintf(f, "%u", el->id);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->ownerUser);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->shareToken);
 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->db);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%u", el->permission);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->targetUser);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->label);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->createdAt);
 if (sep == ',') fputc('"',f);
 fputc(lastSep,f);
 }
 
 /* -------------------------------- End autoSql Generated Code -------------------------------- */
 
 struct myVariantsShare *myVariantsCreateShare(struct sqlConnection *conn,
     char *ownerUser, char *project, char *db, int permission,
     char *targetUser, char *label)
 /* Create a new share record. Returns the share with token and id filled in.
  * Free with myVariantsShareFree. */
 {
 char *token = makeRandomKey(288);  /* 288 bits -> 48 base64 URL-safe chars */
 struct dyString *dy = sqlDyStringCreate(
     "INSERT INTO myVariantsShares (ownerUser, shareToken, project, db, permission, targetUser, label)"
     " VALUES ('%s', '%s', '%s', '%s', %d,",
     ownerUser, token, project, db, permission);
 if (targetUser != NULL)
     sqlDyStringPrintf(dy, " '%s',", targetUser);
 else
     sqlDyStringPrintf(dy, " NULL,");
 if (label != NULL)
     sqlDyStringPrintf(dy, " '%s')", label);
 else
     sqlDyStringPrintf(dy, " NULL)");
 sqlUpdate(conn, dy->string);
 dyStringFree(&dy);
 
 struct myVariantsShare *share;
 AllocVar(share);
 share->id = sqlLastAutoId(conn);
 share->ownerUser = cloneString(ownerUser);
 share->shareToken = token;
 share->project = cloneString(project);
 share->db = cloneString(db);
 share->permission = permission;
 share->targetUser = cloneString(targetUser);
 share->label = cloneString(label);
 share->createdAt = cloneString("");
 return share;
 }
 
 struct myVariantsShare *myVariantsGetShareByToken(struct sqlConnection *conn,
     char *token)
 /* Look up a single share by token. Returns NULL if not found or token is malformed. */
 {
 if (token == NULL || strlen(token) != MYVAR_TOKEN_LENGTH)
     return NULL;
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT * FROM myVariantsShares WHERE shareToken='%s'", token);
 struct sqlResult *sr = sqlGetResult(conn, query);
 char **row = sqlNextRow(sr);
 struct myVariantsShare *share = NULL;
 if (row != NULL)
     share = myVariantsShareLoad(row);
 sqlFreeResult(&sr);
 return share;
 }
 
 struct myVariantsShare *myVariantsGetSharesForOwner(struct sqlConnection *conn,
     char *ownerUser, char *db)
 /* Get all shares created by this user for the given assembly. */
 {
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT * FROM myVariantsShares WHERE ownerUser='%s' AND db='%s'"
     " ORDER BY createdAt DESC", ownerUser, db);
 return myVariantsShareLoadByQuery(conn, query);
 }
 
 struct myVariantsShare *myVariantsGetSharesForUser(struct sqlConnection *conn,
-    char *targetUser, char *db)
-/* Get all shares targeted at this user for the given assembly. */
+    char *userName, char *db)
+/* Get all shares targeted at this user for the given assembly. The targetUser
+ * column holds a normalized comma-separated list, so match with FIND_IN_SET.
+ * BINARY forces a case-sensitive match, matching myVariantsShareAllowsUser. */
 {
 char query[512];
 sqlSafef(query, sizeof(query),
-    "SELECT * FROM myVariantsShares WHERE targetUser='%s' AND db='%s'"
-    " ORDER BY createdAt DESC", targetUser, db);
+    "SELECT * FROM myVariantsShares WHERE FIND_IN_SET(BINARY '%s', targetUser) AND db='%s'"
+    " ORDER BY createdAt DESC", userName, db);
 return myVariantsShareLoadByQuery(conn, query);
 }
 
 boolean myVariantsRevokeShare(struct sqlConnection *conn,
     char *shareToken, char *ownerUser)
 /* Delete a share record. ownerUser must match the share's owner.
  * Returns TRUE if a row was deleted, FALSE if not found or not owner. */
 {
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT count(*) FROM myVariantsShares WHERE shareToken='%s' AND ownerUser='%s'",
     shareToken, ownerUser);
 int count = sqlQuickNum(conn, query);
 if (count == 0)
     return FALSE;
 sqlSafef(query, sizeof(query),
     "DELETE FROM myVariantsShares WHERE shareToken='%s' AND ownerUser='%s'",
     shareToken, ownerUser);
 sqlUpdate(conn, query);
 return TRUE;
 }
 
+static boolean shareOwnedBy(struct sqlConnection *conn, char *shareToken, char *ownerUser)
+/* Return TRUE if a share with this token exists and is owned by ownerUser. */
+{
+char query[512];
+sqlSafef(query, sizeof(query),
+    "SELECT count(*) FROM myVariantsShares WHERE shareToken='%s' AND ownerUser='%s'",
+    shareToken, ownerUser);
+return sqlQuickNum(conn, query) != 0;
+}
+
+boolean myVariantsSetSharePermission(struct sqlConnection *conn,
+    char *shareToken, char *ownerUser, int permission)
+/* Update a share's permission (0=read-only, 1=read-write). ownerUser must
+ * match the share's owner. Returns TRUE if a row was updated, FALSE if not
+ * found or not owner. */
+{
+if (permission != MYVAR_PERM_READONLY && permission != MYVAR_PERM_READWRITE)
+    return FALSE;
+if (!shareOwnedBy(conn, shareToken, ownerUser))
+    return FALSE;
+char query[512];
+sqlSafef(query, sizeof(query),
+    "UPDATE myVariantsShares SET permission=%d WHERE shareToken='%s' AND ownerUser='%s'",
+    permission, shareToken, ownerUser);
+sqlUpdate(conn, query);
+return TRUE;
+}
+
+boolean myVariantsSetShareTargets(struct sqlConnection *conn,
+    char *shareToken, char *ownerUser, char *targetUser)
+/* Update a share's targetUser list (NULL for anyone with link). ownerUser
+ * must match the share's owner. Returns TRUE if a row was updated, FALSE if
+ * not found or not owner. */
+{
+if (!shareOwnedBy(conn, shareToken, ownerUser))
+    return FALSE;
+struct dyString *dy = sqlDyStringCreate("UPDATE myVariantsShares SET targetUser=");
+if (isNotEmpty(targetUser))
+    sqlDyStringPrintf(dy, "'%s'", targetUser);
+else
+    sqlDyStringPrintf(dy, "NULL");
+sqlDyStringPrintf(dy, " WHERE shareToken='%s' AND ownerUser='%s'", shareToken, ownerUser);
+sqlUpdate(conn, dy->string);
+dyStringFree(&dy);
+return TRUE;
+}
+
+boolean myVariantsShareAllowsUser(struct myVariantsShare *share, char *userName)
+/* Return TRUE if userName may access share. TRUE when targetUser is empty
+ * (anyone with link); otherwise TRUE only if userName is non-empty and
+ * appears in the comma-separated targetUser list. NULL-safe. */
+{
+if (share == NULL)
+    return FALSE;
+if (isEmpty(share->targetUser))
+    return TRUE;
+if (isEmpty(userName))
+    return FALSE;
+boolean allowed = FALSE;
+struct slName *names = slNameListFromComma(share->targetUser);
+struct slName *name;
+for (name = names; name != NULL; name = name->next)
+    {
+    trimSpaces(name->name);
+    if (sameString(name->name, userName))
+        {
+        allowed = TRUE;
+        break;
+        }
+    }
+slNameFreeList(&names);
+return allowed;
+}
+
 char *myVariantsShareCartValue(struct myVariantsShare *share)
 /* Build JSON cart value string from a share record.
  * Caller must freeMem the result. */
 {
 struct jsonWrite *jw = jsonWriteNew();
 jsonWriteObjectStart(jw, NULL);
 jsonWriteString(jw, "owner", share->ownerUser);
 jsonWriteString(jw, "project", share->project);
 jsonWriteString(jw, "db", share->db);
 jsonWriteNumber(jw, "permission", share->permission);
 if (isNotEmpty(share->label))
     jsonWriteString(jw, "label", share->label);
 jsonWriteObjectEnd(jw);
 char *result = cloneString(jw->dy->string);
 jsonWriteFree(&jw);
 return result;
 }