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; }