44ccfacbe3a3d4b300f80d48651c77837a4b571e galt Tue Apr 26 11:12:02 2022 -0700 SQL INJECTION Prevention Version 2 - this improves our methods by making subclauses of SQL that get passed around be both easy and correct to use. The way that was achieved was by getting rid of the obscure and not well used functions sqlSafefFrag and sqlDyStringPrintfFrag and replacing them with the plain versions of those functions, since these are not needed anymore. The new version checks for NOSQLINJ in unquoted %-s which is used to include SQL clauses, and will give an error the NOSQLINJ clause is not present, and this will automatically require the correct behavior by developers. sqlDyStringPrint is a very useful function, however because it was not enforced, users could use various other dyString functions and they operated without any awareness or checking for SQL correct use. Now those dyString functions are prohibited and it will produce an error if you try to use a dyString function on a SQL string, which is simply detected by the presence of the NOSQLINJ prefix. diff --git src/hg/hgTracks/searchTracks.c src/hg/hgTracks/searchTracks.c index 4609c28..4c0c6da 100644 --- src/hg/hgTracks/searchTracks.c +++ src/hg/hgTracks/searchTracks.c @@ -1,1247 +1,1244 @@ /* Track search code used by hgTracks CGI */ /* Copyright (C) 2012 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include <pthread.h> #include "common.h" #include "search.h" #include "hCommon.h" #include "memalloc.h" #include "obscure.h" #include "dystring.h" #include "hash.h" #include "cheapcgi.h" #include "hPrint.h" #include "htmshell.h" #include "cart.h" #include "hgTracks.h" #include "web.h" #include "jksql.h" #include "hdb.h" #include "mdb.h" #include "fileUi.h" #include "trix.h" #include "jsHelper.h" #include "imageV2.h" #include "hgConfig.h" #include "trackHub.h" #include "hubConnect.h" #include "hubPublic.h" #include "hubSearchText.h" #include "errCatch.h" #define TRACK_SEARCH_FORM "trackSearch" #define SEARCH_RESULTS_FORM "searchResults" #define TRACK_SEARCH_CURRENT_TAB "tsCurTab" #define TRACK_SEARCH_SIMPLE "tsSimple" #define TRACK_SEARCH_ON_NAME "tsName" #define TRACK_SEARCH_ON_TYPE "tsType" #define TRACK_SEARCH_ON_GROUP "tsGroup" #define TRACK_SEARCH_ON_DESCR "tsDescr" #define TRACK_SEARCH_SORT "tsSort" #define TRACK_SEARCH_ON_HUBS "tsIncludePublicHubs" // the list of found tracks struct slRef *tracks = NULL; // for advanced search only: // associates hub id's to hub urls that have search results, // used to get huburls onto hgTrackUi links struct hash *hubIdsToUrls = NULL; static int gCmpGroup(const void *va, const void *vb) /* Compare groups based on label. */ { const struct group *a = *((struct group **)va); const struct group *b = *((struct group **)vb); return strcmp(a->label, b->label); } // Would like to do a radio button choice ofsorts enum sortBy { sbRelevance=0, sbAbc =1, sbHierarchy=2, }; static int gCmpTrackHierarchy(const void *va, const void *vb) /* Compare tracks based on longLabel. */ { const struct slRef *aa = *((struct slRef **)va); const struct slRef *bb = *((struct slRef **)vb); const struct track *a = ((struct track *) aa->val); const struct track *b = ((struct track *) bb->val); if ( tdbIsFolder(a->tdb) && !tdbIsFolder(b->tdb)) return -1; else if (!tdbIsFolder(a->tdb) && tdbIsFolder(b->tdb)) return 1; if ( tdbIsContainer(a->tdb) && !tdbIsContainer(b->tdb)) return -1; else if (!tdbIsContainer(a->tdb) && tdbIsContainer(b->tdb)) return 1; if (!tdbIsContainerChild(a->tdb) && tdbIsContainerChild(b->tdb)) return -1; else if ( tdbIsContainerChild(a->tdb) && !tdbIsContainerChild(b->tdb)) return 1; return strcasecmp(a->longLabel, b->longLabel); } static int gCmpTrack(const void *va, const void *vb) /* Compare tracks based on longLabel. */ { const struct slRef *aa = *((struct slRef **)va); const struct slRef *bb = *((struct slRef **)vb); const struct track *a = ((struct track *) aa->val); const struct track *b = ((struct track *) bb->val); return strcasecmp(a->longLabel, b->longLabel); } static void findTracksSort(struct slRef **pTrack, enum sortBy sortBy) { if (sortBy == sbHierarchy) slSort(pTrack, gCmpTrackHierarchy); else if (sortBy == sbAbc) slSort(pTrack, gCmpTrack); else slReverse(pTrack); } static int getFormatTypes(char ***pLabels, char ***pTypes) { char *crudeTypes[] = { ANYLABEL, "bam", "psl", "chain", "netAlign", "maf", "bed", "bigBed", "ctgPos", "expRatio", "genePred", "broadPeak", "narrowPeak", "rmsk", "bedGraph", "bigWig", "wig", "wigMaf" }; // Non-standard: // type altGraphX // type axt // type bed5FloatScore // type bed5FloatScoreWithFdr // type chromGraph // type clonePos // type coloredExon // type encodeFiveC // type factorSource // type ld2 // type logo // type maf // type sample // type wigMafProt 0.0 1.0 char *nicerTypes[] = { ANYLABEL, "Alignment binary (bam) - binary SAM", "Alignment Blast (psl) - Blast output", "Alignment Chains (chain) - Pairwise alignment", "Alignment Nets (netAlign) - Net alignments", "Alignments (maf) - multiple alignment format", "bed - browser extensible data", "bigBed - self index, often remote bed format", "ctgPos - Contigs", "expRatio - Expression ratios", "Genes (genePred) - Gene prediction and annotation", "Peaks Broad (broadPeak) - ENCODE large region peak format", "Peaks Narrow (narrowPeak) - ENCODE small region peak format", "Repeats (rmsk) - Repeat masking", "Signal (bedGraph) - graphically represented bed data", "Signal (bigWig) - self index, often remote wiggle format", "Signal (wig) - wiggle format", "Signal (wigMaf) - multiple alignment wiggle" }; int ix = 0, count = sizeof(crudeTypes)/sizeof(char *); char **labels; char **values; AllocArray(labels, count); AllocArray(values, count); for (ix=0;ix<count;ix++) { labels[ix] = cloneString(nicerTypes[ix]); values[ix] = cloneString(crudeTypes[ix]); } *pLabels = labels; *pTypes = values; return count; } static struct trackDb *getSuperTrackTdbs(struct trackDb *tdbList) /* Supertracks are not in the main trackDbList returned by hubAddTracks, find them * here since our search hits may hit them */ { struct hash *superTrackHash = hashNew(0); struct trackDb *tdb, *ret = NULL; for (tdb = tdbList; tdb != NULL; tdb = tdb->next) { if (tdbIsSuperTrackChild(tdb) && hashFindVal(superTrackHash, tdb->parent->track) == NULL) { hashAdd(superTrackHash, tdb->parent->track, tdb->parent); } } struct hashEl *hel, *helList = hashElListHash(superTrackHash); for (hel = helList; hel != NULL; hel = hel->next) slAddHead(&ret, (struct trackDb *)hel->val); return ret; } static void hubTdbListToTrackList(struct trackDb *tdbList, struct track **trackList, struct slName *trackNames) /* Recursively convert a (struct trackDb *) to a (struct track *) */ { struct trackDb *tmp, *next; for (tmp = tdbList; tmp != NULL; tmp = next) { next = tmp->next; if (slNameInList(trackNames, tmp->track)) { struct track *t = trackFromTrackDb(tmp); slAddHead(trackList, t); } if (tmp->subtracks) hubTdbListToTrackList(tmp->subtracks, trackList, trackNames); } } static void hubTdbListAddSupers(struct trackDb *tdbList, struct track **trackList, struct slName *trackNames) /* a track we are looking for might be a super track and thus not in tdbList, look for it here */ { struct trackDb *tmp, *superTrackDbs = getSuperTrackTdbs(tdbList); for (tmp = superTrackDbs; tmp != NULL; tmp = tmp->next) { if (slNameInList(trackNames, tmp->track)) { struct track *tg = trackFromTrackDb(tmp); slAddHead(trackList, tg); } } } struct hubSearchTracks /* A helper struct for collapsing a (struct hubSearchText *) into just the parts * we need for looking up the track hits */ { struct hubSearchTracks *next; char *hubUrl; // the url to this hub which is used as a key into the search hash int hubId; struct hubConnectStatus *hub; // the hubStatus result struct slName *searchedTracks; // the track names the search terms matched against }; struct paraFetchData /* A helper struct for managing connecting to many hubs in parallel and adding the * relevant tracks to the global (struct slRef *)tracks struct. */ { struct paraFetchData *next; char *hubName; // the name of the hub for measureTiming results struct hubSearchTracks *hst; // the tracks we are adding to the search results struct track *tlist; // the resulting tracks to add to the global trackList pthread_t *threadId; // so we can stop the thread if it has been taking too long long searchTime; // how many milliseconds did it take to search this hub boolean done; }; // helper variables for connecting to hubs in parallel pthread_mutex_t pfdMutex = PTHREAD_MUTEX_INITIALIZER; struct paraFetchData *pfdListInitial = NULL; struct paraFetchData *pfdList = NULL; struct paraFetchData *pfdRunning = NULL; struct paraFetchData *pfdDone = NULL; void *addUnconnectedHubSearchResults(void *threadParam) /* Add a not yet connected hub to the search results */ { pthread_t *thread = threadParam; struct paraFetchData *pfd = NULL; // this thread will just happily keep working until waitForSearchResults() finishes, // moving it's completed work onto pfdDone, so we can safely detach pthread_detach(*thread); boolean allDone = FALSE; while(1) { pthread_mutex_lock(&pfdMutex); // the wait function will set pfdList = NULL, so don't start up any more // stuff if that happens: if (!pfdList) allDone = TRUE; else { pfd = slPopHead(&pfdList); pfd->threadId = threadParam; slAddHead(&pfdRunning, pfd); } pthread_mutex_unlock(&pfdMutex); if (allDone) return NULL; struct hubSearchTracks *hst = pfd->hst; struct track *tracksToAdd = NULL; long startTime = clock1000(); struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { pfd->done = FALSE; struct trackDb *tdbList = hubAddTracks(hst->hub, database); if (measureTiming) measureTime("After connecting to hub %s: '%d': ", hst->hubUrl, hst->hubId); // get composite and subtracks into trackList hubTdbListToTrackList(tdbList, &tracksToAdd, hst->searchedTracks); hubTdbListAddSupers(tdbList, &tracksToAdd, hst->searchedTracks); pfd->done = TRUE; pfd->tlist = tracksToAdd; pfd->searchTime = clock1000() - startTime;; } errCatchEnd(errCatch); pthread_mutex_lock(&pfdMutex); slRemoveEl(&pfdRunning, pfd); slAddHead(&pfdDone, pfd); if (measureTiming) measureTime("Finished finding tracks for hub '%s': ", pfd->hubName); pthread_mutex_unlock(&pfdMutex); } //always return NULL for pthread_create() return NULL; } static void hubSearchHashToPfdList(char *descSearch, struct hash *searchResultsHash, struct hash *hubLookup, struct sqlConnection *conn) /* getHubSearchResults() returned a hash of search hits to various hubs, convert that * into something we can work on in parallel */ { struct hubSearchTracks *ret = NULL; struct hashCookie cookie = hashFirst(searchResultsHash); struct hash *hubUrlsToTrackList = hashNew(0); struct hashEl *hel = NULL; struct dyString *trackName = dyStringNew(0); struct slName *connectedHubs = hubConnectHubsInCart(cart); while ((hel = hashNext(&cookie)) != NULL) { struct hubSearchText *hst = (struct hubSearchText *)hel->val; struct hubEntry *hubInfo = (struct hubEntry *) hashFindVal(hubLookup, hst->hubUrl); if (isNotEmpty(hubInfo->errorMessage)) continue; // if we were already connected to this hub, then it's search hits // were already taken care of by the regular search code char hubId[256]; safef(hubId, sizeof(hubId), "%d", hubInfo->id); if (slNameInList(connectedHubs, hubId)) continue; struct hubConnectStatus *hub = hubConnectStatusForId(conn, hubInfo->id); // the hubSearchText contains multiple entries per lookup in order to form // the hgHubConnect UI. We only need one type of hit here: struct hubSearchTracks *found = hashFindVal(hubUrlsToTrackList, hst->hubUrl); for (; hst != NULL; hst = hst->next) { // don't add results for matches to the hub descriptionUrl, only to track names/descs if (isNotEmpty(hst->track) && hst->textLength != hubSearchTextMeta) { // hst->textLength=hubSearchTextLong denotes hits to track description if (isNotEmpty(descSearch) && hst->textLength != hubSearchTextLong) continue; if (!found) { AllocVar(found); found->hubUrl = hst->hubUrl; found->searchedTracks = NULL; found->hub = hub; found->hubId = hubInfo->id; slAddHead(&ret, found); hashAdd(hubUrlsToTrackList, hst->hubUrl, found); hashAdd(hubIdsToUrls, hubId, hst->hubUrl); } dyStringPrintf(trackName, "%s%d_%s", hubTrackPrefix, hubInfo->id, hst->track); slNameStore(&found->searchedTracks, cloneString(trackName->string)); dyStringClear(trackName); } } } struct hubSearchTracks *t; for (t = ret; t != NULL; t = t->next) { struct paraFetchData *pfd; AllocVar(pfd); pfd->hubName = t->hubUrl; pfd->hst = t; slAddHead(&pfdList, pfd); slAddHead(&pfdListInitial, CloneVar(pfd)); } if (measureTiming) measureTime("Finished converting hubSearchText to hubSearchTracks and pfd"); } void waitForSearchResults(int numThreads, pthread_t *threadList) /* Run each thread and kill the ones that take too long */ { // only wait 5 seconds, if something is in the cache we can show it // otherwise just ignore, nobody should wait more than 5 seconds // for a simple track search. Although note that just getting to // this point can take quite a while depending on hgcentral // connections and obtaining trackDb. int maxTime = 5 * 1000; int waitTime = 0; int lockStatus = 0; struct paraFetchData *pfd; while(1) { sleep1000(50); waitTime += 50; boolean allDone = TRUE; // we don't want to block in the event one of the child threads is // taking forever lockStatus = pthread_mutex_trylock(&pfdMutex); if (pfdList || pfdRunning) allDone = FALSE; if (allDone) { if (lockStatus == 0) // we aquired the lock pthread_mutex_unlock(&pfdMutex); break; } if (waitTime >= maxTime) { if (lockStatus == 0) // we aquired the lock pthread_mutex_unlock(&pfdMutex); break; } if (lockStatus == 0) // release the lock if we got it pthread_mutex_unlock(&pfdMutex); } // now that we've waited the maximum time we need to kill // any running threads and add the results of any threads // that ran successfully lockStatus = pthread_mutex_trylock(&pfdMutex); struct paraFetchData *neverRan = pfdList; if (lockStatus == 0) { // prevent still running threads from continuing pfdList = NULL; if (measureTiming) fprintf(stdout, "<span class='timing'>Successfully aquired lock, adding any succesful thread data\n<br></span>"); for (pfd = pfdDone; pfd != NULL; pfd = pfd->next) { struct track *t; for (t = pfd->tlist; t != NULL; t = t->next) refAdd(&tracks, t); if (measureTiming) measureTime("'%s' search times", pfd->hubName); } for (pfd = pfdRunning; pfd != NULL; pfd = pfd->next) { pthread_cancel(*pfd->threadId); if (measureTiming) measureTime("'%s' search times: timed out", pfd->hubName); } for (pfd = neverRan; pfd != NULL; pfd = pfd->next) if (measureTiming) measureTime("'%s' search times: never ran", pfd->hubName); } else { // Should we warn or something that results are still waiting? As of now // just silently return instead, and note that no unconnected hub data // will show up (we get connected hub results for free because of // trackDbCaching) if (measureTiming) measureTime("Timed out searching hubs"); } if (lockStatus == 0) pthread_mutex_unlock(&pfdMutex); } void addHubSearchResults(struct slName *nameList, char *descSearch) /* add public hubs to the track list */ { struct sqlConnection *conn = hConnectCentral(); char *hubSearchTableName = hubSearchTextTableName(); char *publicTable = cfgOptionEnvDefault("HGDB_HUB_PUBLIC_TABLE", hubPublicTableConfVariable, defaultHubPublicTableName); char *statusTable = cfgOptionEnvDefault("HGDB_HUB_STATUS_TABLE", hubStatusTableConfVariable, defaultHubStatusTableName); struct dyString *extra = dyStringNew(0); if (nameList) { struct slName *tmp = NULL; - char escapedInput[512]; for (tmp = nameList; tmp != NULL; tmp = tmp->next) { - // escape user input manually: - sqlSafefFrag(escapedInput, sizeof(escapedInput), "%s", tmp->name); - dyStringPrintf(extra, "label like '%%%s%%'", escapedInput); + dyStringPrintf(extra, "label like '%%%s%%'", tmp->name); if (tmp->next) dyStringPrintf(extra, " and "); } } if (sqlTableExists(conn, hubSearchTableName)) { struct hash *searchResultsHash = hashNew(0); struct hash *pHash = hashNew(0); struct slName *hubsToPrint = NULL; addPublicHubsToHubStatus(cart, conn, publicTable, statusTable); struct hash *hubLookup = buildPublicLookupHash(conn, publicTable, statusTable, &pHash); char *db = cloneString(trackHubSkipHubName(database)); tolowers(db); getHubSearchResults(conn, hubSearchTableName, descSearch, isNotEmpty(descSearch), db, hubLookup, &searchResultsHash, &hubsToPrint, dyStringCannibalize(&extra)); hubSearchHashToPfdList(descSearch, searchResultsHash, hubLookup, conn); if (measureTiming) measureTime("after querying hubSearchText table and ready to start threads"); int ptMax = atoi(cfgOptionDefault("parallelFetch.threads", "20")); int pfdListCount = 0, pt; if (ptMax > 0) { pfdListCount = slCount(pfdList); pthread_t *threads = NULL; ptMax = min(ptMax, pfdListCount); if (ptMax > 0) { AllocArray(threads, ptMax); for (pt = 0; pt < ptMax; pt++) { int rc = pthread_create(&threads[pt], NULL, addUnconnectedHubSearchResults, &threads[pt]); if (rc ) errAbort("Unexpected error in pthread_create"); } } waitForSearchResults(ptMax, threads); } } if (measureTiming) measureTime("Total time spent searching hubs"); } static void simpleSearchForTracks(char *simpleEntry) // Performs the simple search and returns the found tracks. { // Prepare for trix search if (!isEmpty(simpleEntry)) { int trixWordCount = 0; char *tmp = cloneString(simpleEntry); char *val = nextWord(&tmp); struct slName *el, *trixList = NULL; while (val != NULL) { slNameAddTail(&trixList, val); trixWordCount++; val = nextWord(&tmp); } if (trixWordCount > 0 && !isHubTrack(database)) { // Unfortunately trixSearch can't handle the slName list int i; char **trixWords = needMem(sizeof(char *) * trixWordCount); for (i = 0, el = trixList; el != NULL; i++, el = el->next) trixWords[i] = strLower(el->name); // Now open the trix file char trixFile[HDB_MAX_PATH_STRING]; getSearchTrixFile(database, trixFile, sizeof(trixFile)); struct trix *trix = trixOpen(trixFile); struct trixSearchResult *tsList = trixSearch(trix, trixWordCount, trixWords, tsmExpand); for ( ; tsList != NULL; tsList = tsList->next) { struct track *track = (struct track *) hashFindVal(trackHash, tsList->itemId); if (track != NULL) // It is expected that this is NULL { // (e.g. when trix references trackDb tracks which have no tables) refAdd(&tracks, track); } } //trixClose(trix); // don't bother (this is a CGI that is about to end) } } } static void advancedSearchForTracks(struct sqlConnection *conn,struct group *groupList, char *nameSearch, char *typeSearch, char *descSearch, char *groupSearch, struct slPair *mdbPairs, boolean includeHubResults) // Performs the advanced search and returns the found tracks. { int tracksFound = 0; int numMetadataNonEmpty = 0; struct slPair *pair = mdbPairs; for (; pair!= NULL;pair=pair->next) { if (!isEmpty((char *)(pair->val))) numMetadataNonEmpty++; } if (!isEmpty(groupSearch) && sameString(groupSearch,ANYLABEL)) groupSearch = NULL; if (!isEmpty(typeSearch) && sameString(typeSearch,ANYLABEL)) typeSearch = NULL; if (isEmpty(nameSearch) && isEmpty(typeSearch) && isEmpty(descSearch) && isEmpty(groupSearch) && numMetadataNonEmpty == 0) return; // First do the metaDb searches, which can be done quickly for all tracks with db queries. struct hash *matchingTracks = NULL; if (numMetadataNonEmpty) { struct mdbObj *mdbObj, *mdbObjs = mdbObjRepeatedSearch(conn,mdbPairs,TRUE,FALSE); if (mdbObjs) { for (mdbObj = mdbObjs; mdbObj != NULL; mdbObj = mdbObj->next) { if (matchingTracks == NULL) matchingTracks = newHash(0); hashAddInt(matchingTracks, mdbObj->obj, 1); } mdbObjsFree(&mdbObjs); } if (matchingTracks == NULL) return; } // Set the word lists up once struct slName *nameList = NULL; if (!isEmpty(nameSearch)) nameList = slNameListOfUniqueWords(cloneString(nameSearch),TRUE); // TRUE means respect quotes struct slName *descList = NULL; if (!isEmpty(descSearch)) descList = slNameListOfUniqueWords(cloneString(descSearch),TRUE); struct group *group; for (group = groupList; group != NULL; group = group->next) { if (isEmpty(groupSearch) || sameString(group->name, groupSearch)) { if (group->trackList == NULL) continue; struct trackRef *tr; for (tr = group->trackList; tr != NULL; tr = tr->next) { struct track *track = tr->track; char *trackType = cloneFirstWord(track->tdb->type); // will be spilled if ((matchingTracks == NULL || hashLookup(matchingTracks, track->track) != NULL) && ( isEmpty(typeSearch) || (sameWord(typeSearch, trackType) && !tdbIsComposite(track->tdb))) && (isEmpty(nameSearch) || searchNameMatches(track->tdb, nameList)) && (isEmpty(descSearch) || searchDescriptionMatches(track->tdb, descList))) { if (track != NULL) { tracksFound++; refAdd(&tracks, track); } else warn("found group track is NULL."); } if (track->subtracks != NULL) { struct track *subTrack; for (subTrack = track->subtracks; subTrack != NULL; subTrack = subTrack->next) { trackType = cloneFirstWord(subTrack->tdb->type); // will be spilled if ( (matchingTracks == NULL || hashLookup(matchingTracks, subTrack->track) != NULL) && (isEmpty(typeSearch) || sameWord(typeSearch, trackType)) && (isEmpty(nameSearch) || searchNameMatches(subTrack->tdb, nameList)) && (isEmpty(descSearch) // subtracks inherit description || searchDescriptionMatches(subTrack->tdb, descList) || (tdbIsCompositeChild(subTrack->tdb) && subTrack->parent && searchDescriptionMatches(subTrack->parent->tdb, descList)))) { if (track != NULL) { tracksFound++; refAdd(&tracks, subTrack); } else warn("found subtrack is NULL."); } } } } } } if (measureTiming) measureTime("searched native tracks: "); if (includeHubResults) addHubSearchResults(nameList,descSearch); } #define MAX_FOUND_TRACKS 100 static void findTracksPageLinks(int tracksFound, int startFrom, int instance) { char id[256]; if (tracksFound <= MAX_FOUND_TRACKS) return; // Opener int willStartAt = 0; int curPage = (startFrom/MAX_FOUND_TRACKS) + 1; int endAt = startFrom+MAX_FOUND_TRACKS; if (endAt > tracksFound) endAt = tracksFound; hPrintf("<span><em style='font-size:.9em;'>Listing %d - %d of %d tracks</em> ", startFrom+1,endAt,tracksFound); // << and < if (startFrom >= MAX_FOUND_TRACKS) { safef(id, sizeof id, "ftpl%d-first", instance); hPrintf("<a href='../cgi-bin/hgTracks?%s=Search&%s=0' id='%s' title='First page of found tracks'" ">«</a> ", TRACK_SEARCH,TRACK_SEARCH_PAGER,id); jsOnEventByIdF("click", id, "return findTracks.page(\"%s\",0);", TRACK_SEARCH_PAGER); safef(id, sizeof id, "ftpl%d-prev", instance); willStartAt = startFrom - MAX_FOUND_TRACKS; hPrintf(" <a href='../cgi-bin/hgTracks?%s=Search&%s=%d' id='%s' " "title='Previous page of found tracks'>‹</a> ", TRACK_SEARCH,TRACK_SEARCH_PAGER,willStartAt,id); jsOnEventByIdF("click", id, "return findTracks.page(\"%s\",%d);", TRACK_SEARCH_PAGER,willStartAt); } // page number links int lastPage = (tracksFound/MAX_FOUND_TRACKS); if ((tracksFound % MAX_FOUND_TRACKS) > 0) lastPage++; int thisPage = curPage - 3; // Window of 3 pages above and below if (thisPage < 1) thisPage = 1; for (;thisPage <= lastPage && thisPage <= curPage + 3; thisPage++) { safef(id, sizeof id, "ftpl%d-%d", instance, thisPage); if (thisPage != curPage) { willStartAt = ((thisPage - 1) * MAX_FOUND_TRACKS); endAt = willStartAt+ MAX_FOUND_TRACKS; if (endAt > tracksFound) endAt = tracksFound; hPrintf(" <a href='../cgi-bin/hgTracks?%s=Search&%s=%d' id='%s' " "title='Page %d (%d - %d) tracks'>%d</a> ", TRACK_SEARCH,TRACK_SEARCH_PAGER,willStartAt,id,thisPage,willStartAt+1,endAt,thisPage); jsOnEventByIdF("click", id, "return findTracks.page(\"%s\",%d);",TRACK_SEARCH_PAGER,willStartAt); } else hPrintf(" <em style='color:%s;'>%d</em> ",COLOR_DARKGREY,thisPage); } // > and >> if ((startFrom + MAX_FOUND_TRACKS) < tracksFound) { safef(id, sizeof id, "ftpl%d-next", instance); willStartAt = startFrom + MAX_FOUND_TRACKS; hPrintf(" <a href='../cgi-bin/hgTracks?%s=Search&%s=%d' id='%s' " "title='Next page of found tracks'>›</a> ", TRACK_SEARCH,TRACK_SEARCH_PAGER,willStartAt,id); jsOnEventByIdF("click", id, "return findTracks.page(\"%s\",%d);",TRACK_SEARCH_PAGER,willStartAt); safef(id, sizeof id, "ftpl%d-last", instance); willStartAt = tracksFound - (tracksFound % MAX_FOUND_TRACKS); if (willStartAt == tracksFound) willStartAt -= MAX_FOUND_TRACKS; hPrintf(" <a href='../cgi-bin/hgTracks?%s=Search&%s=%d' id='%s' title='Last page of found tracks' " ">»</a></span>\n", TRACK_SEARCH,TRACK_SEARCH_PAGER,willStartAt,id); jsOnEventByIdF("click", id, "return findTracks.page(\"%s\",%d);",TRACK_SEARCH_PAGER,willStartAt); } } static void displayFoundTracks(struct cart *cart, struct slRef *tracks, int tracksFound, enum sortBy sortBy) // Routine for displaying found tracks { char id[256]; char javascript[1024]; hPrintf("<div id='found' style='display:none;'>\n"); // This div is emptied with 'clear' button if (tracksFound < 1) { hPrintf("<p>No tracks found</p>\n"); } else { hPrintf("<form action='%s' name='%s' id='%s' method='post'>\n\n", hgTracksName(),SEARCH_RESULTS_FORM,SEARCH_RESULTS_FORM); cartSaveSession(cart); // Creates hidden var of hgsid to avoid bad voodoo int startFrom = 0; hPrintf("<table id='foundTracks'>\n"); // Opening view in browser button and foundTracks count #define ENOUGH_FOUND_TRACKS 10 if (tracksFound >= ENOUGH_FOUND_TRACKS) { hPrintf("<tr><td nowrap colspan=3>\n"); hPrintf("<INPUT TYPE=SUBMIT NAME='submit' VALUE='return to browser' class='viewBtn' " "style='font-size:.8em;'>"); hPrintf(" <span class='selCbCount'></span>\n"); startFrom = cartUsualInt(cart,TRACK_SEARCH_PAGER,0); if (startFrom > 0 && startFrom < tracksFound) { int countUp = 0; for (countUp=0; countUp < startFrom;countUp++) { if (slPopHead(&tracks) == NULL) // memory waste break; } } hPrintf("</td><td align='right' valign='bottom'>\n"); findTracksPageLinks(tracksFound,startFrom,0); hPrintf("</td></tr>\n"); } // Begin foundTracks table //hPrintf("<table id='foundTracks'><tr><td colspan='2'>\n"); hPrintf("<tr><td colspan='2'>\n"); hPrintf("</td><td align='right'>\n"); hPrintf("</td></tr><tr bgcolor='#%s'><td>",HG_COL_HEADER); #define PM_BUTTON \ "<IMG height=18 width=18 " \ "id='btn_%s' src='../images/%s' title='%s all found tracks'>" hPrintf(PM_BUTTON,"plus_all", "add_sm.gif", "Select"); hPrintf(PM_BUTTON,"minus_all","remove_sm.gif","Unselect"); jsOnEventById("click", "btn_plus_all", "return findTracks.checkAllWithWait(true);"); jsOnEventById("click", "btn_minus_all", "return findTracks.checkAllWithWait(false);"); hPrintf("</td><td><b>Visibility</b></td><td colspan=2> <b>Track Name</b>\n"); // Sort options? if (tracksFound >= ENOUGH_FOUND_TRACKS) { hPrintf("<span style='float:right;'>Sort:"); cgiMakeOnEventRadioButtonWithClass(TRACK_SEARCH_SORT, "0", (sortBy == sbRelevance), NULL,"click", "findTracks.sortNow(this);"); hPrintf("by Relevance"); cgiMakeOnEventRadioButtonWithClass(TRACK_SEARCH_SORT, "1", (sortBy == sbAbc), NULL,"click", "findTracks.sortNow(this);"); hPrintf("Alphabetically"); cgiMakeOnEventRadioButtonWithClass(TRACK_SEARCH_SORT, "2", (sortBy == sbHierarchy), NULL,"click", "findTracks.sortNow(this);"); hPrintf("by Hierarchy </span>\n"); } hPrintf("</td></tr>\n"); // Set up json for js functionality struct jsonElement *jsonTdbVars = newJsonObject(newHash(8)); int trackCount=0; boolean containerTrackCount = 0; struct slRef *ptr; struct slName *connectedHubIds = hubConnectHubsInCart(cart); while((ptr = slPopHead(&tracks))) { if (++trackCount > MAX_FOUND_TRACKS) break; struct track *track = (struct track *) ptr->val; jsonTdbSettingsBuild(jsonTdbVars, track, FALSE); // FALSE: No config from track search char *hubId = NULL; if (isHubTrack(track->track)) { char *trackNameCopy = cloneString(track->track); hubId = strchr(trackNameCopy, '_'); hubId += 1; char *ptr2 = strchr(hubId, '_'); if (ptr2 == NULL) errAbort("hub track '%s' not in correct format", track->track); *ptr2 = '\0'; char *hubUrl = hashFindVal(hubIdsToUrls, hubId); if (hubUrl != NULL) { struct jsonElement *ele = jsonFindNamedField(jsonTdbVars, "", track->track); jsonObjectAdd(ele, "hubUrl", newJsonString(hubUrl)); } } if (tdbIsFolder(track->tdb)) // supertrack hPrintf("<tr class='bgLevel4' valign='top' class='found'>\n"); else if (tdbIsContainer(track->tdb)) hPrintf("<tr class='bgLevel3' valign='top' class='found'>\n"); else hPrintf("<tr class='bgLevel2' valign='top' class='found'>\n"); hPrintf("<td align='center'>\n"); // Determine visibility and checked state track->visibility = tdbVisLimitedByAncestors(cart, track->tdb, TRUE, TRUE); boolean checked = ( track->visibility != tvHide ); if (tdbIsContainerChild(track->tdb)) { // Don't need all 4 states here. Visible=checked&&enabled checked = fourStateVisible(subtrackFourStateChecked(track->tdb,cart)); // Checked is only if subtrack level vis is also set! checked = (checked && ( track->visibility != tvHide )); } // if we haven't already connected to this hub, then by default // we need to unselect every checkbox if (isHubTrack(track->track) && !slNameInList(connectedHubIds, hubId)) checked = FALSE; // Setup the check box #define CB_HIDDEN_VAR "<INPUT TYPE=HIDDEN disabled=true NAME='%s_sel' VALUE='%s'>" // subtracks and folder children get "_sel" var. ("_sel" var is temp on folder children) if (tdbIsContainerChild(track->tdb) || tdbIsFolderContent(track->tdb)) hPrintf(CB_HIDDEN_VAR,track->track,checked?"1":CART_VAR_EMPTY); #define CB_SEEN "<INPUT TYPE=CHECKBOX id='%s_sel_id' VALUE='on' class='selCb' %s>" hPrintf(CB_SEEN,track->track,(checked ? " CHECKED" : "")); safef(id, sizeof id, "%s_sel_id", track->track); // XSS Filter? jsOnEventById("click", id, "findTracks.clickedOne(this,true);"); hPrintf("</td><td>\n"); // Setup the visibility drop down #define VIS_HIDDEN_VAR "<INPUT TYPE=HIDDEN disabled=true NAME='%s' VALUE='%s'>" hPrintf(VIS_HIDDEN_VAR,track->track,CART_VAR_EMPTY); // All tracks get vis hidden var safef(id, sizeof id, "%s_id", track->track); // XSS Filter? safef(javascript, sizeof javascript, "findTracks.changeVis(this);"); struct slPair *event = slPairNew("change", cloneString(javascript)); if (tdbIsFolder(track->tdb)) { hideShowDropDownWithClassAndExtra(track->track, id, (track->visibility != tvHide), "normalText visDD", event); } else { hTvDropDownClassWithJavascript(NULL, id, track->visibility,track->canPack, "normalText seenVis",event); } // If this is a container track, allow configuring... if (tdbIsContainer(track->tdb) || tdbIsFolder(track->tdb)) { containerTrackCount++; // Using onclick ensures return to search tracks on submit hPrintf(" <IMG SRC='../images/folderWrench.png' style='cursor:pointer;' " "id='%s_confSet' title='Configure this track container...' " "> ", track->track); safef(id, sizeof id, "%s_confSet", track->track); // XSS Filter? char hubConfigUrl[4096]; safef(hubConfigUrl, sizeof(hubConfigUrl), "%s", track->track); if (isHubTrack(track->track)) { char *hubUrl = hashFindVal(hubIdsToUrls, hubId); if (hubUrl != NULL) safefcat(hubConfigUrl, sizeof(hubConfigUrl), "&hubUrl=%s", hubUrl); } jsOnEventByIdF("click", id, "findTracks.configSet(\"%s\");", hubConfigUrl); } //#define SHOW_PARENT_FOLDER #ifdef SHOW_PARENT_FOLDER else if (tdbIsContainerChild(track->tdb) || tdbIsFolderContent(track->tdb)) { struct trackDb *parentTdb = tdbIsContainerChild(track->tdb) ? tdbGetContainer(track->tdb) : tdbGetImmediateFolder(track->tdb); if (parentTdb != NULL) // Using href will not return to search tracks on submit hPrintf(" <A HREF='../cgi-bin/hgTrackUi?g=%s'><IMG SRC='../images/folderC.png'" " title='Navigate to parent container...'></A> ", parentTdb->track); } #endif///def SHOW_PARENT_FOLDER hPrintf("</td>\n"); // shortLabel has description popup and longLabel has "..." metadata char hgTrackUiUrl[4096]; safef(hgTrackUiUrl, sizeof(hgTrackUiUrl), "%s", trackUrl(track->track, NULL)); if (isHubTrack(track->track)) { char *hubUrl = hashFindVal(hubIdsToUrls, hubId); if (hubUrl != NULL) safefcat(hgTrackUiUrl, sizeof(hgTrackUiUrl), "&hubUrl=%s", hubUrl); } hPrintf("<td><a target='_top' id='%s_dispFndTrk' " "href='%s' title='Display track details'>%s</a></td>\n", track->track, hgTrackUiUrl, track->shortLabel); safef(id, sizeof id, "%s_dispFndTrk", track->track); jsOnEventByIdF("click", id, "popUp.hgTrackUi('%s',true); return false;", track->track); hPrintf("<td>%s", track->longLabel); compositeMetadataToggle(database, track->tdb, NULL, TRUE, FALSE); hPrintf("</td></tr>\n"); } //hPrintf("</table>\n"); // Closing view in browser button and foundTracks count hPrintf("<tr><td nowrap colspan=3>"); hPrintf("<INPUT TYPE=SUBMIT NAME='submit' VALUE='Return to Browser' class='viewBtn' " "style='font-size:.8em;'>"); hPrintf(" <span class='selCbCount'></span>"); if (tracksFound >= ENOUGH_FOUND_TRACKS) { hPrintf("</td><td align='right' valign='top'>\n"); findTracksPageLinks(tracksFound,startFrom,1); hPrintf("</td></tr>\n"); } hPrintf("</table>\n"); if (containerTrackCount > 0) hPrintf("<BR><IMG SRC='../images/folderWrench.png'> Tracks so marked are containers " "which group related data tracks. Containers may need additional configuration " "(by clicking on the <IMG SRC='../images/folderWrench.png'> icon) before they can " "be viewed in the browser.<BR>\n"); //hPrintf("* Tracks so marked are containers which group related data tracks. These may " // "not be visible unless further configuration is done. Click on the * to " // "configure these.<BR><BR>\n"); hPrintf("\n</form>\n"); // be done with json jsonTdbSettingsUse(jsonTdbVars); } hPrintf("</div>"); // This div allows the clear button to empty it } void doSearchTracks(struct group *groupList) { webIncludeResourceFile("ui.dropdownchecklist.css"); jsIncludeFile("ui.dropdownchecklist.js",NULL); // This line is needed to get the multi-selects initialized jsIncludeFile("ddcl.js",NULL); struct group *group; char *groups[128]; char *labels[128]; int numGroups = 1; groups[0] = ANYLABEL; labels[0] = ANYLABEL; char *nameSearch = cartOptionalString(cart, TRACK_SEARCH_ON_NAME); char *typeSearch = cartUsualString( cart, TRACK_SEARCH_ON_TYPE,ANYLABEL); char *simpleEntry = cartOptionalString(cart, TRACK_SEARCH_SIMPLE); char *descSearch = cartOptionalString(cart, TRACK_SEARCH_ON_DESCR); char *groupSearch = cartUsualString( cart, TRACK_SEARCH_ON_GROUP,ANYLABEL); boolean doSearch = sameString(cartOptionalString(cart, TRACK_SEARCH), "Search") || cartUsualInt(cart, TRACK_SEARCH_PAGER, -1) >= 0; boolean includeHubResults = cartUsualBoolean(cart, TRACK_SEARCH_ON_HUBS, FALSE); struct sqlConnection *conn = NULL; boolean metaDbExists = FALSE; if (!isHubTrack(database)) { conn = hAllocConn(database); metaDbExists = sqlTableExists(conn, "metaDb"); } int tracksFound = 0; int cols; char buf[512]; char *currentTab = cartUsualString(cart, TRACK_SEARCH_CURRENT_TAB, "simpleTab"); enum searchTab selectedTab = (sameString(currentTab, "advancedTab") ? advancedTab : simpleTab); // NOTE: could support quotes in simple tab by detecting quotes and choosing // to use doesNameMatch() || doesDescriptionMatch() if (selectedTab == simpleTab && !isEmpty(simpleEntry)) stripChar(simpleEntry, '"'); trackList = getTrackList(&groupList, -2); // global makeGlobalTrackHash(trackList); // NOTE: This is necessary when container cfg by '*' results in vis changes // This will handle composite/view override when subtrack specific vis exists, // AND superTrack reshaping. // Subtrack settings must be removed when composite/view settings are updated parentChildCartCleanup(trackList,cart,oldVars); slSort(&groupList, gCmpGroup); for (group = groupList; group != NULL; group = group->next) { groupTrackListAddSuper(cart, group); if (group->trackList != NULL) { groups[numGroups] = cloneString(group->name); labels[numGroups] = cloneString(group->label); numGroups++; if (numGroups >= ArraySize(groups)) internalErr(); } } safef(buf, sizeof(buf),"Search for Tracks in the %s %s Assembly", organism, hFreezeFromDb(database)); webStartWrapperDetailedNoArgs(cart, database, "", buf, FALSE, FALSE, FALSE, FALSE); hPrintf("<div style='max-width:1080px;'>"); hPrintf("<form action='%s' name='%s' id='%s' method='get'>\n\n", hgTracksName(),TRACK_SEARCH_FORM,TRACK_SEARCH_FORM); cartSaveSession(cart); // Creates hidden var of hgsid to avoid bad voodoo safef(buf, sizeof(buf), "%lu", clock1()); cgiMakeHiddenVar("hgt_", buf); // timestamps page to avoid browser cache hPrintf("<input type='hidden' name='db' value='%s'>\n", database); hPrintf("<input type='hidden' name='%s' id='currentTab' value='%s'>\n", TRACK_SEARCH_CURRENT_TAB, currentTab); hPrintf("<input type='hidden' name='%s' value=''>\n",TRACK_SEARCH_DEL_ROW); hPrintf("<input type='hidden' name='%s' value=''>\n",TRACK_SEARCH_ADD_ROW); hPrintf("<input type='hidden' name='%s' value=''>\n",TRACK_SEARCH_PAGER); hPrintf("<div id='tabs' style='display:none; %s'>\n<ul>\n<li><a href='#simpleTab'>" "<B style='font-size:.9em;font-family: arial, Geneva, Helvetica, san-serif;'>Search</B>" "</a></li>\n<li><a href='#advancedTab'>" "<B style='font-size:.9em;font-family: arial, Geneva, Helvetica, san-serif;'>Advanced</B>" "</a></li>\n</ul>\n<div id='simpleTab' style='max-width:inherit;'>\n", cgiBrowser()==btIE?"width:1060px;":"max-width:inherit;"); hPrintf("<table id='simpleTable' style='width:100%%; font-size:.9em;'><tr><td colspan='2'>"); hPrintf("<input type='text' name='%s' id='simpleSearch' class='submitOnEnter' value='%s' " "style='max-width:1000px; width:100%%;'>\n", TRACK_SEARCH_SIMPLE,simpleEntry == NULL ? "" : simpleEntry); jsOnEventById("keyup", "simpleSearch", "findTracks.searchButtonsEnable(true);"); hPrintf("</td></tr><td style='max-height:4px;'></td></tr></table>"); //hPrintf("</td></tr></table>"); hPrintf("<input type='submit' name='%s' id='searchSubmit' value='search' " "style='font-size:.8em;'>\n", TRACK_SEARCH); hPrintf("<input type='button'id='doSTClear1' name='clear' value='clear' class='clear' " "style='font-size:.8em;'>\n"); jsOnEventById("click", "doSTClear1", "findTracks.clear();"); hPrintf("<input type='submit' name='submit' value='cancel' class='cancel' " "style='font-size:.8em;'>\n"); hPrintf("</div>\n"); // Advanced tab hPrintf("<div id='advancedTab' style='width:inherit;'>\n" "<table id='advancedTable' cellSpacing=0 style='width:inherit; font-size:.9em;'>\n"); cols = 8; // Track Name contains hPrintf("<tr><td colspan=3></td>"); hPrintf("<td nowrap><b style='max-width:100px;'>Track Name:</b></td>"); hPrintf("<td align='right'>contains</td>\n"); hPrintf("<td colspan='%d'>", cols - 4); hPrintf("<input type='text' name='%s' id='nameSearch' class='submitOnEnter' value='%s' " "style='min-width:326px; font-size:.9em;'>", TRACK_SEARCH_ON_NAME, nameSearch == NULL ? "" : nameSearch); jsOnEventById("keyup", "nameSearch", "findTracks.searchButtonsEnable(true);"); hPrintf("</td></tr>\n"); // Description contains hPrintf("<tr><td colspan=2></td><td align='right'>and </td>"); hPrintf("<td><b style='max-width:100px;'>Description:</b></td>"); hPrintf("<td align='right'>contains</td>\n"); hPrintf("<td colspan='%d'>", cols - 4); hPrintf("<input type='text' name='%s' id='descSearch' value='%s' class='submitOnEnter' " "style='max-width:536px; width:536px; font-size:.9em;'>", TRACK_SEARCH_ON_DESCR, descSearch == NULL ? "" : descSearch); jsOnEventById("keyup", "descSearch", "findTracks.searchButtonsEnable(true);"); hPrintf("</td></tr>\n"); hPrintf("<tr><td colspan=2></td><td align='right'>and </td>\n"); hPrintf("<td><b style='max-width:100px;'>Group:</b></td>"); hPrintf("<td align='right'>is</td>\n"); hPrintf("<td colspan='%d'>", cols - 4); cgiMakeDropListFullExt(TRACK_SEARCH_ON_GROUP, labels, groups, numGroups, groupSearch, NULL, NULL, "min-width:40%; font-size:.9em;", "groupSearch"); hPrintf("</td></tr>\n"); // Track Type is (drop down) hPrintf("<tr><td colspan=2></td><td align='right'>and </td>\n"); hPrintf("<td nowrap><b style='max-width:100px;'>Data Format:</b></td>"); hPrintf("<td align='right'>is</td>\n"); hPrintf("<td colspan='%d'>", cols - 4); char **formatTypes = NULL; char **formatLabels = NULL; int formatCount = getFormatTypes(&formatLabels, &formatTypes); cgiMakeDropListFullExt(TRACK_SEARCH_ON_TYPE, formatLabels, formatTypes, formatCount, typeSearch, NULL, NULL, "'min-width:40%; font-size:.9em;", "typeSearch"); hPrintf("</td></tr>\n"); // include public hubs in output: hPrintf("<tr><td colspan=2></td><td align='right'>and </td>\n"); hPrintf("<td nowrap><b style='max-width:100px;'>Include Public Hub Search Results:</b></td>"); hPrintf("<td></td>"); hPrintf("<td colspan='%d'>", cols - 4); cgiMakeCheckBox(TRACK_SEARCH_ON_HUBS, includeHubResults); hPrintf("NOTE: Including public hubs in results may be slow"); hPrintf("</td></tr>\n"); // mdb selects struct slPair *mdbSelects = NULL; if (metaDbExists) { struct slPair *mdbVars = mdbVarsSearchable(conn,TRUE,FALSE); // Tables but not file only objects mdbSelects = mdbSelectPairs(cart, mdbVars); char *output = mdbSelectsHtmlRows(conn,mdbSelects,mdbVars,cols,FALSE); // not a fileSearch if (output) { puts(output); freeMem(output); } slPairFreeList(&mdbVars); } hPrintf("</table>\n"); hPrintf("<input type='submit' name='%s' id='searchSubmit' value='search' " "style='font-size:.8em;'>\n", TRACK_SEARCH); hPrintf("<input type='button' id='doSTClear2' name='clear' value='clear' class='clear' " "style='font-size:.8em;'>\n"); jsOnEventById("click", "doSTClear2", "findTracks.clear();"); hPrintf("<input type='submit' name='submit' value='cancel' class='cancel' " "style='font-size:.8em;'>\n"); //hPrintf("<a target='_blank' href='../goldenPath/help/trackSearch.html'>help</a>\n"); hPrintf("</div>\n"); hPrintf("</div>\n"); hPrintf("</form>\n"); hPrintf("</div>"); // Restricts to max-width:1000px; cgiDown(0.8); if (measureTiming) measureTime("Rendered tabs"); if (doSearch) { // Now search long startTime = clock1000(); if (hubIdsToUrls == NULL) hubIdsToUrls = hashNew(0); if (selectedTab==simpleTab && !isEmpty(simpleEntry)) simpleSearchForTracks(simpleEntry); else if (selectedTab==advancedTab) advancedSearchForTracks(conn,groupList,nameSearch,typeSearch,descSearch, groupSearch,mdbSelects, includeHubResults); if (measureTiming) fprintf(stdout, "<span class='timing'>Searched for tracks: %lu<br></span>", clock1000()-startTime); // Sort and Print results if (selectedTab!=filesTab) { enum sortBy sortBy = cartUsualInt(cart,TRACK_SEARCH_SORT,sbRelevance); tracksFound = slCount(tracks); if (tracksFound > 1) findTracksSort(&tracks,sortBy); displayFoundTracks(cart,tracks,tracksFound,sortBy); if (measureTiming) measureTime("Displayed found tracks"); } slPairFreeList(&mdbSelects); } hFreeConn(&conn); webNewSection("About Track Search"); if (metaDbExists) hPrintf("<p>Search for terms in track names, descriptions, groups, and ENCODE " "metadata. If multiple terms are entered, only tracks with all terms " "will be part of the results."); else hPrintf("<p>Search for terms in track descriptions, groups, and names. " "If multiple terms are entered, only tracks with all terms " "will be part of the results."); hPrintf("<BR><a target='_blank' href='../goldenPath/help/trackSearch.html'>more help</a></p>\n"); webEndSectionTables(); }