5e26e73c8e71bfdcc0b96ee6ec3b480ea04867ec chmalee Mon May 4 08:37:03 2026 -0700 Add missing file for myVariants feature, refs build email diff --git src/hg/hgTracks/myVariantsTrack.c src/hg/hgTracks/myVariantsTrack.c new file mode 100644 index 00000000000..f668f784a1e --- /dev/null +++ src/hg/hgTracks/myVariantsTrack.c @@ -0,0 +1,921 @@ +/* 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 "jsonWrite.h" + +static char *reservedFieldNames[] = { + "bin", "chrom", "chromStart", "chromEnd", "name", "score", "strand", + "thickStart", "thickEnd", "itemRgb", "description", "db", "ref", "alt", + "project", "mouseover", "id", NULL +}; + +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; +} + +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) + return; + +/* 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; + item->chromStart = item->thickStart = hgp->singlePos->chromStart; + item->chromEnd = item->thickEnd = hgp->singlePos->chromEnd; + item->strand[0] = '.'; + item->score = 0; + item->bin = binFromRange(item->chromStart, item->chromEnd); + item->name = hgp->singlePos->name; + item->ref = cloneString(""); + item->alt = cloneString(""); + item->db = database; + item->description = cloneString(hgp->singlePos->description); + item->project = cloneString(""); + item->mouseover = 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->name = cloneString(pos->name ? pos->name : hgvs); + item->ref = cloneString(""); + item->alt = cloneString(""); + item->db = database; + item->description = cloneString(pos->description ? pos->description : ""); + item->project = cloneString(""); + item->mouseover = 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", ""); + unsigned color; + htmlColorForCode(colorCode, &color); + + item->bin = binFromRange(chromStart, chromEnd); + item->chrom = cloneString(chrom); + item->chromStart = chromStart; + item->chromEnd = chromEnd; + item->thickStart = thickStart; + item->thickEnd = thickEnd; + item->name = cloneString(name); + item->score = score; + item->strand[0] = strand[0]; + item->itemRgb = color; + item->description = cloneString(description); + item->ref = cloneString(ref); + item->alt = cloneString(alt); + item->db = database; + item->project = cloneString(project); + item->mouseover = cloneString(mouseover); + } + +if (!item) + return; + +/* 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); + +/* Trigger CT import by writing a concrete CT file to trash and pointing ctfile_<db> to it */ +char *ctFile = myVariantsWriteCtFile(userName, database, cart); +if (isNotEmpty(ctFile)) + { + char varName[256]; + safef(varName, sizeof varName, CT_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) +/* Update text valued field with new val. */ +{ +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)) + { + char sql[256]; + sqlSafef(sql, sizeof(sql), "update %s set %s='%d' where id=%d", + tableName, fieldName, color, id); + sqlUpdate(conn, sql); + cartRemove(cart, varName); + } + return; + } +struct dyString *sql = sqlDyStringCreate("update %s set %s='%s' where id=%d", + tableName, fieldName, newVal, id); +sqlUpdate(conn, sql->string); +dyStringFree(&sql); +cartRemove(cart, varName); +} + +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); + /* Trigger CT refresh */ + char *userName = getUserName(); + if (userName) + { + char *ctFile = myVariantsWriteCtFile(userName, database, cart); + if (isNotEmpty(ctFile)) + { + char ctVar[256]; + safef(ctVar, sizeof ctVar, CT_FILE_VAR_PREFIX "%s", database); + cartSetString(cart, ctVar, ctFile); + freeMem(ctFile); + } + } + return; + } + + /* Handle edits. */ + updateTextField(trackName, conn, tableName, "name", id); + updateTextField(trackName, conn, tableName, "description", id); + updateTextField(trackName, conn, tableName, "itemRgb", id); + updateTextField(trackName, conn, tableName, "chromStart", id); + updateTextField(trackName, conn, tableName, "chromEnd", id); + updateTextField(trackName, conn, tableName, "ref", id); + updateTextField(trackName, conn, tableName, "alt", id); + updateTextField(trackName, conn, tableName, "project", id); + updateTextField(trackName, conn, tableName, "mouseover", id); + /* 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); + slFreeList(&customCols); + } + /* Trigger CT refresh after edits */ + { + char *userName = getUserName(); + if (userName) + { + char *ctFile = myVariantsWriteCtFile(userName, database, cart); + if (isNotEmpty(ctFile)) + { + char ctVar[256]; + safef(ctVar, sizeof ctVar, CT_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; +int rowOffset; +struct sqlResult *sr = hRangeQuery(conn, tableName, chromName, winStart, winEnd, + whereExtra, &rowOffset); +char **row; +struct dyString *mouseover = dyStringNew(0); +while ((row = sqlNextRow(sr)) != NULL) + { + struct myVariants *item = myVariantsLoad(row); + struct bed *bed; + AllocVar(bed); + 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; + lf = bedMungToLinkedFeatures(&bed, tdb, 9, 0, 1000, TRUE); + dyStringClear(mouseover); + if (item->mouseover && isNotEmpty(item->mouseover)) + lf->mouseOver = cloneString(item->mouseover); + else + { + dyStringPrintf(mouseover, "%s", buf); + 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 = startsWith("myVariants_shared_", track->track); + +if (isShared) + { + /* Extract the token and look up the share info from the cart */ + char *token = track->track + strlen("myVariants_shared_"); + char cartVar[256]; + safef(cartVar, sizeof(cartVar), MYVAR_SHARED_CART_PREFIX "%s", token); + char *cartVal = cartOptionalString(cart, cartVar); + if (isEmpty(cartVal)) + return; + char *owner = NULL, *project = NULL, *db = NULL; + int permission = 0; + if (!myVariantsParseShareCartValue(cartVal, &owner, &project, &db, &permission, NULL)) + return; + char *tableName = myVariantsGetDbTable(owner); + if (isEmpty(tableName)) + { + freeMem(owner); + freeMem(project); + freeMem(db); + return; + } + struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); + struct dyString *whereClause = sqlDyStringCreate("db='%s'", database); + if (!sameString(project, "*")) + sqlDyStringPrintf(whereClause, " and project='%s'", project); + lfList = loadMyVariantsItems(conn, tableName, track->tdb, + dyStringCannibalize(&whereClause)); + hFreeConn(&conn); + freeMem(owner); + freeMem(project); + freeMem(db); + } +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 name of one of an item to display on left side. */ +{ +struct linkedFeatures *lf = item; +char *name = lf->name; +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; +} + +struct trackDb *myVariantsFakeTdb() +/* Construct a trackDb record for myVariants track. */ +{ +char *userName = getUserName(); +struct trackDb *tdb = customTrackTdbDefault(); +char *tableName = myVariantsCreateTable(userName); +if (!tableName) + errAbort("Error creating myVariants table for user '%s'", userName); +tdb->track = "myVariants"; +tdb->table = cloneString(tableName); +tdb->type = "myVariants"; +trackDbAddSetting(tdb, "mouseOverField", "description"); +return tdb; +} + +struct track *myVariantsTg() +/* Make track that will ultimately have the data from: + * CUSTOM_TRASH.myVariants_userName table */ +{ +struct trackDb *tdb = myVariantsFakeTdb(); +struct track *tg = trackFromTrackDb(tdb); +myVariantsMethods(tg); +return tg; +} + +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 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"); +} + +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"); + /* 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"); + } + struct myVariantsShare *share = myVariantsCreateShare(conn, userName, + project, db, permission, targetUser, label); + 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 + { + 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. */ +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)) + { + 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_%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; + + /* Re-validate the share against hgcentral on every edit. The cart-cached + * permission is not authoritative: the owner may have revoked the share + * or downgraded it from read-write to read-only since the recipient's + * cart was last set, and we don't want stale cart contents to grant + * writes that the live share record no longer permits. */ + struct sqlConnection *centralConn = hConnectCentral(); + if (!sqlTableExists(centralConn, "myVariantsShares")) + { + hDisconnectCentral(¢ralConn); + continue; + } + struct myVariantsShare *liveShare = myVariantsGetShareByToken(centralConn, token); + hDisconnectCentral(¢ralConn); + if (liveShare == NULL) + continue; + if (liveShare->permission != MYVAR_PERM_READWRITE) + { + myVariantsShareFree(&liveShare); + continue; + } + if (isNotEmpty(liveShare->targetUser) && !sameString(liveShare->targetUser, userName)) + { + myVariantsShareFree(&liveShare); + continue; + } + char *owner = cloneString(liveShare->ownerUser); + myVariantsShareFree(&liveShare); + + char *tableName = myVariantsGetDbTable(owner); + if (isEmpty(tableName)) + { + freeMem(owner); + 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); + freeMem(owner); + continue; + } + + /* Apply field edits — no delete, no project change */ + struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); + updateTextField(trackName, conn, tableName, "name", id); + updateTextField(trackName, conn, tableName, "description", id); + updateTextField(trackName, conn, tableName, "itemRgb", id); + updateTextField(trackName, conn, tableName, "chromStart", id); + updateTextField(trackName, conn, tableName, "chromEnd", id); + updateTextField(trackName, conn, tableName, "ref", id); + updateTextField(trackName, conn, tableName, "alt", id); + updateTextField(trackName, conn, tableName, "mouseover", id); + struct slName *customCols = myVariantsGetCustomFields(owner); + struct slName *col; + for (col = customCols; col != NULL; col = col->next) + updateTextField(trackName, conn, tableName, col->name, id); + slFreeList(&customCols); + hFreeConn(&conn); + freeMem(owner); + } +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); +}