fe99a2d141b3f2d33e8706c1061d59015e0c571d
hiram
  Thu Jun 4 14:39:19 2026 -0700
correct detection of login status and correct use of local hgcentraltest when not on one of the RR servers refs #31811

diff --git src/hg/hubApi/liftOver.c src/hg/hubApi/liftOver.c
index 274b64ede17..3c655490ab1 100644
--- src/hg/hubApi/liftOver.c
+++ src/hg/hubApi/liftOver.c
@@ -1,547 +1,560 @@
 /* liftOver functions */
 
 #include "dataApi.h"
 #include "hgFind.h"
 #include "cartTrackDb.h"
 #include "cartJson.h"
 #include "genark.h"
 #include "asmAlias.h"
 #include "assemblyList.h"
 #include "liftOver.h"
 #include "liftOverChain.h"
 #include "net.h"
 #include "wikiLink.h"
 #include "userdata.h"
 
 /**** SHOULD BE IN LIBRARY - code from hgConvert.c ******/
 static long chainTotalBlockSize(struct chain *chain)
 /* Return sum of sizes of all blocks in chain */
 {
 struct cBlock *block;
 long total = 0;
 for (block = chain->blockList; block != NULL; block = block->next)
     total += block->tEnd - block->tStart;
 return total;
 }
 /**** SHOULD BE IN LIBRARY - code from hgConvert.c ******/
 
 static void chainListOut(char *fromDb, char* toDb, int origSize, char *fromPos, struct chain *chainList)
 /* given the list of chains, output the list in JSON */
 {
 struct chain *chain = NULL;
 char position[4096];
 char coverage[4096];
 struct jsonWrite *jw = apiStartOutput();
 jsonWriteListStart(jw, "from");
 jsonWriteListStart(jw, NULL);
 jsonWriteString(jw, NULL, fromDb);
 jsonWriteString(jw, NULL, fromPos);
 jsonWriteListEnd(jw);
 jsonWriteListEnd(jw);
 jsonWriteListStart(jw, "to");
 for (chain = chainList; chain != NULL; chain = chain->next)
     {
     int blockSize;
     int qStart, qEnd;
     if (chain->qStrand == '-')
         {
         qStart = chain->qSize - chain->qEnd;
         qEnd = chain->qSize - chain->qStart;
         }
     else
         {
         qStart = chain->qStart;
         qEnd = chain->qEnd;
         }
     blockSize = chainTotalBlockSize(chain);
     safef(position, sizeof(position), "%s:%d-%d", chain->qName, qStart+1, qEnd);
     safef(coverage, sizeof(coverage), "%3.1f%% of bases, %3.1f%% of span",
         100.0 * blockSize / origSize,
         100.0 * (chain->tEnd - chain->tStart) / origSize);
     jsonWriteListStart(jw, NULL);
     jsonWriteString(jw, NULL, toDb);
     jsonWriteString(jw, NULL, position);
     jsonWriteString(jw, NULL, coverage);
     jsonWriteListEnd(jw);
     }
 jsonWriteListEnd(jw);
 apiFinishOutput(0, NULL, jw);
 }
 
 static void listExisting()
 /* output the fromDb,toDb from liftOverChain.hgcentral SQL table */
 {
 char *filter = cgiOptionalString(argFilter);
 char *fromDb = cgiOptionalString(argFromGenome);
 char *toDb = cgiOptionalString(argToGenome);
 
 struct sqlConnection *conn = hConnectOtto();
 char *tableName = cloneString(liftOverChainTable());
 struct dyString *query = newDyString(0);
 sqlDyStringPrintf(query, "SELECT count(*) FROM %s", tableName);
 long long totalRows = sqlQuickLongLong(conn, dyStringContents(query));
 dyStringClear(query);
 
 if (isNotEmpty(fromDb) && isNotEmpty(toDb))
     {
     /* match a chain recorded in either direction */
     sqlDyStringPrintf(query, "SELECT * FROM %s WHERE "
         "(LOWER(fromDb) = LOWER('%s') AND LOWER(toDb) = LOWER('%s')) "
         "OR (LOWER(fromDb) = LOWER('%s') AND LOWER(toDb) = LOWER('%s'))",
         tableName, fromDb, toDb, toDb, fromDb);
     }
 else if (isNotEmpty(fromDb) || isNotEmpty(toDb))
     {
     sqlDyStringPrintf(query, "SELECT * FROM %s WHERE ", tableName);
     if (isNotEmpty(fromDb))
         sqlDyStringPrintf(query, "LOWER(fromDb) = LOWER('%s') ", fromDb);
     if (isNotEmpty(toDb))
         sqlDyStringPrintf(query, "LOWER(toDb) = LOWER('%s') ", toDb);
     }
 else if (isNotEmpty(filter))
     {
     sqlDyStringPrintf(query, "SELECT * FROM %s WHERE LOWER(fromDb) = LOWER('%s') OR LOWER(toDb) = LOWER('%s')", tableName, filter, filter);
     }
 else
     {
     sqlDyStringPrintf(query, "SELECT * FROM %s", tableName);
     }
 sqlDyStringPrintf(query, " LIMIT %d;", maxItemsOutput);
 
 char *dataTime = sqlTableUpdate(conn, tableName);
 time_t dataTimeStamp = sqlDateToUnixTime(dataTime);
 replaceChar(dataTime, ' ', 'T');	/* ISO 8601 */
 struct jsonWrite *jw = apiStartOutput();
 jsonWriteString(jw, "dataTime", dataTime);
 jsonWriteNumber(jw, "dataTimeStamp", (long long)dataTimeStamp);
 
 jsonWriteListStart(jw, "existingLiftOvers");
 struct liftOverChain *chain, *chainList = liftOverChainLoadByQuery(conn, dyStringCannibalize(&query));
 for (chain = chainList; chain != NULL; chain = chain->next)
     {
     jsonWriteObjectStart(jw, NULL);
     jsonWriteString(jw, "fromDb", chain->fromDb);
     jsonWriteString(jw, "toDb", chain->toDb);
     jsonWriteString(jw, "path", chain->path);
     jsonWriteDouble(jw, "minMatch", chain->minMatch);
     jsonWriteNumber(jw, "minChainT", chain->minChainT);
     jsonWriteNumber(jw, "minSizeQ", chain->minSizeQ);
     jsonWriteString(jw, "multiple", chain->multiple);
     jsonWriteDouble(jw, "minBlocks", chain->minBlocks);
     jsonWriteString(jw, "fudgeThick", chain->fudgeThick);
     jsonWriteObjectEnd(jw);
     }
 jsonWriteListEnd(jw);
 jsonWriteNumber(jw, "totalLiftOvers", totalRows);
 int chainListCount = slCount(chainList);
 jsonWriteNumber(jw, "itemsReturned", chainListCount);
 liftOverChainFreeList(&chainList);
 
 /* if no chain rows for this pair, check ottoRequest for any existing
  * row (any status) so the user is told their pair has already been
  * submitted instead of being allowed to create a duplicate row */
 if (chainListCount == 0 && isNotEmpty(fromDb) && isNotEmpty(toDb))
     {
     char *ottoTable = cfgOption("ottoTable");
     if (isNotEmpty(ottoTable) && sqlTableExists(conn, ottoTable))
         {
         struct dyString *pq = newDyString(0);
         sqlDyStringPrintf(pq,
             "SELECT id, status, requestTime FROM %s "
             "WHERE requestType='liftOver' AND "
             "((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s')) "
             "ORDER BY requestTime DESC LIMIT 1",
             ottoTable, fromDb, toDb, toDb, fromDb);
         char **row;
         struct sqlResult *sr = sqlGetResult(conn, dyStringCannibalize(&pq));
         if ((row = sqlNextRow(sr)) != NULL)
             {
             jsonWriteBoolean(jw, "pending", TRUE);
             jsonWriteNumber(jw, "pendingStatus", sqlSigned(row[1]));
             jsonWriteString(jw, "pendingRequestTime", row[2]);
             }
         sqlFreeResult(&sr);
         }
     }
 
 apiFinishOutput(0, NULL, jw);
 hDisconnectOtto(&conn);
 }
 
 static void loginStatus()
 /* output current user login status as JSON */
 {
-char *userName = (loginSystemEnabled() || wikiLinkEnabled()) ? wikiLinkUserName() : NULL;
+/* wikiLinkUserName() handles all cookie validation internally */
+char *userName = wikiLinkUserName();
 struct jsonWrite *jw = apiStartOutput();
 char hgLoginLink[2048];
+boolean privateHost = hIsPrivateHost();
+/* can not use hgcentral hglogin from hgwdev/genome-test */
+if (privateHost)
+    safef(hgLoginLink, sizeof(hgLoginLink), "%shgLogin", hLocalHostCgiBinUrl());
+else
     safef(hgLoginLink, sizeof(hgLoginLink), "%shgLogin", hLoginHostCgiBinUrl());
 
 if (userName != NULL)
     {
     // Get both email and realName from gbMembers table
-    struct sqlConnection *sc = hConnectOtto();
+    struct sqlConnection *sc = NULL;
+    if (privateHost)
+	sc = hConnectCentral();
+    else
+	sc = hConnectOtto();
     struct dyString *query = sqlDyStringCreate("select email, realName from gbMembers where userName = '%s'", userName);
     struct sqlResult *sr = sqlGetResult(sc, dyStringCannibalize(&query));
     char **row = sqlNextRow(sr);
 
     char *email = NULL;
     char *realName = NULL;
     if (row != NULL)
         {
         email = cloneString(row[0] ? row[0] : "");
         realName = cloneString(row[1] ? row[1] : "");
         }
     sqlFreeResult(&sr);
+    if (privateHost)
+	hDisconnectCentral(&sc);
+    else
 	hDisconnectOtto(&sc);
 
     // Build logout URL with returnto parameter
     char *returnTo = cgiOptionalString("returnTo");
     struct dyString *logoutUrl = dyStringNew(0);
     dyStringPrintf(logoutUrl, "%s?hgLogin.do.displayLogout=1", hgLoginLink);
     if (isNotEmpty(returnTo))
         {
         char *encodedReturnUrl = cgiEncodeFull(returnTo);
 
         dyStringPrintf(logoutUrl, "&returnto=%s", encodedReturnUrl);
         freeMem(encodedReturnUrl);
         }
 
     jsonWriteString(jw, "userName", userName);
     jsonWriteString(jw, "email", email ? email : "");
     jsonWriteString(jw, "realName", realName ? realName : "");
     jsonWriteString(jw, "logoutUrl", dyStringCannibalize(&logoutUrl));
 
     if (email)
         freeMem(email);
     if (realName)
         freeMem(realName);
     }
 else
     {
     jsonWriteString(jw, "userName", NULL);
     // Use returnTo parameter passed by calling JavaScript
     char *returnTo = cgiOptionalString("returnTo");
     struct dyString *loginUrl = dyStringNew(0);
     dyStringPrintf(loginUrl, "%s?hgLogin.do.displayLoginPage=1", hgLoginLink);
     struct dyString *signUpUrl = dyStringNew(0);
     dyStringPrintf(signUpUrl, "%s?hgLogin.do.displaySignupPage=1", hgLoginLink);
     if (isNotEmpty(returnTo))
         {
         char *encodedReturnUrl = cgiEncodeFull(returnTo);
         dyStringPrintf(loginUrl, "&returnto=%s", encodedReturnUrl);
         jsonWriteString(jw, "loginUrl", dyStringCannibalize(&loginUrl));
         freeMem(encodedReturnUrl);
         }
     else
         {
         // No returnTo provided, just give basic login URL
         jsonWriteString(jw, "loginUrl", dyStringCannibalize(&loginUrl));
         }
     jsonWriteString(jw, "signupUrl", dyStringCannibalize(&signUpUrl));
     }
 
 apiFinishOutput(0, NULL, jw);
 }
 
 /**** SHOULD BE IN LIBRARY - code from hgConvert.c ******/
 static char *skipWord(char *fw)
 /* skips over current word to start of next.
  * Error for this not to exist. */
 {
 char *s;
 s = skipToSpaces(fw);
 if (s == NULL)
     errAbort("Expecting two words in .ra file line %s\n", fw);
 s = skipLeadingSpaces(s);
 if (s == NULL)
     errAbort("Expecting two words in .ra file line %s\n", fw);
 return s;
 }
 
 static struct chain *chainLoadIntersecting(char *fileName,
 	char *chrom, int start, int end)
 /* Load the chains that intersect given region. */
 {
 struct lineFile *lf = netLineFileOpen(fileName);
 char *line;
 int chromNameSize = strlen(chrom);
 struct chain *chainList = NULL, *chain;
 #ifdef SOON	/* Put in if we index. */
 boolean gotChrom = FALSE;
 #endif  /* SOON */
 int chainCount = 0;
 
 while (lineFileNextReal(lf, &line))
     {
     if (startsWith("chain", line) && isspace(line[5]))
         {
 	++chainCount;
 	line = skipWord(line);	/* Skip over 'chain' */
 	line = skipWord(line);	/* Skip over chain score */
 	if (startsWith(chrom, line) && isspace(line[chromNameSize]))
 	    {
 #ifdef SOON	/* Put in if we index. */
 	    gotChrom = TRUE;
 #endif  /* SOON */
 	    lineFileReuse(lf);
 	    chain = chainReadChainLine(lf);
 	    if (rangeIntersection(chain->tStart, chain->tEnd, start, end) > 0)
 		{
 		chainReadBlocks(lf, chain);
 		slAddHead(&chainList, chain);
 		}
 	    else
 	        chainFree(&chain);
 	    }
 #ifdef SOON	/* Put in if we index. */
 	else if (gotChrom)
 	    break;	/* We assume file is sorted by chromosome, so we're done. */
 #endif /* SOON */
 	}
     }
 lineFileClose(&lf);
 slReverse(&chainList);
 return chainList;
 }
 
 static struct chain *chainLoadAndTrimIntersecting(char *fileName,
 	char *chrom, int start, int end)
 /* Load the chains that intersect given region, and trim them
  * to fit region. */
 {
 struct chain *rawList, *chainList = NULL, *chain, *next;
 rawList = chainLoadIntersecting(fileName, chrom, start, end);
 for (chain = rawList; chain != NULL; chain = next)
     {
     struct chain *subChain, *chainToFree;
     next = chain->next;
     chainSubsetOnT(chain, start, end, &subChain, &chainToFree);
     if (subChain != NULL)
 	slAddHead(&chainList, subChain);
     if (chainToFree != NULL)
         chainFree(&chain);
     }
 slSort(&chainList, chainCmpScore);
 return chainList;
 }
 /**** SHOULD BE IN LIBRARY - code from hgConvert.c ******/
 
 void apiLiftOver(char *words[MAX_PATH_INFO])
 /* 'liftOver' function words[1] is the subCommand */
 {
 char *extraArgs = verifyLegalArgs(argLiftOver);
 if (extraArgs)
     apiErrAbort(err400, err400Msg, "extraneous arguments found for function /liftOver '%s'", extraArgs);
 
 if (sameWordOk("listExisting", words[1]))
     {
     listExisting();
     return;
     }
 else if (sameWordOk("loginStatus", words[1]))
     {
     loginStatus();
     return;
     }
 
 char *fromGenome = cgiOptionalString(argFromGenome);
 char *toGenome = cgiOptionalString(argToGenome);
 char *chrom = cgiOptionalString(argChrom);
 char *start = cgiOptionalString(argStart);
 char *end = cgiOptionalString(argEnd);
 
 if (isEmpty(fromGenome) || isEmpty(toGenome) || isEmpty(chrom) || isEmpty(start) || isEmpty(end))
     apiErrAbort(err400, err400Msg, "must have all arguments: %s, %s, %s, %s, %s  for endpoint '/liftOver", argFromGenome, argToGenome, argChrom, argStart, argEnd);
 
 unsigned uStart = 0;
 unsigned uEnd = 0;
 uStart = sqlUnsigned(start);
 uEnd = sqlUnsigned(end);
 if (uEnd < uStart)
     apiErrAbort(err400, err400Msg, "given start coordinate %u is greater than given end coordinate", uStart, uEnd);
 
 struct dbDb *fromDb = hDbDb(fromGenome);
 if (fromDb == NULL)
     {
     fromDb = genarkLiftOverDb(fromGenome);
     }
 if (fromDb == NULL)
     apiErrAbort(err400, err400Msg, "can not find 'fromGenome=%s' for endpoint '/liftOver", fromGenome);
 struct dbDb *toDb = hDbDb(toGenome);
 if (toDb == NULL)
     {
     toDb = genarkLiftOverDb(toGenome);
     }
 if (toDb == NULL)
     apiErrAbort(err400, err400Msg, "can not find 'toGenome=%s' for endpoint '/liftOver", toGenome);
 
 char *fileName = liftOverChainFile(fromDb->name, toDb->name);
 if (isEmpty(fileName))
     apiErrAbort(err400, err400Msg, "Unable to find a chain file from %s to %s - please contact support", fromGenome, toGenome);
 fileName = hReplaceGbdbMustDownload(fileName);
 char fromPos[4096];
 safef(fromPos, sizeof(fromPos), "%s:%u-%u", chrom, uStart, uEnd);
 char *nChrom;
 int nStart, nEnd;
 if (!hgParseChromRange(NULL, fromPos, &nChrom, &nStart, &nEnd))
     apiErrAbort(err400, err400Msg, "position %s is not in chrom:start-end format", fromPos);
 int origSize = nEnd - nStart;
 struct chain *chainList = chainLoadAndTrimIntersecting(fileName, nChrom, nStart, nEnd);
 if (chainList == NULL)
     apiErrAbort(err400, err400Msg, "Sorry, this position %s is not found in the %s assembly", fromPos, toGenome);
 chainListOut(fromGenome, toGenome, origSize, fromPos, chainList);
 }	/*	void apiLiftOver(char *words[MAX_PATH_INFO])	*/
 
 // char *argLiftRequest[] = {argFromGenome, argToGenome, argEmail, argComment, NULL};
 void apiLiftRequest(char *words[MAX_PATH_INFO])
 /* 'liftOver' function words[1] is the subCommand */
 {
 char *extraArgs = verifyLegalArgs(argLiftRequest);
 if (extraArgs)
     apiErrAbort(err400, err400Msg, "extraneous arguments found for function /liftRequest '%s'", extraArgs);
 
 char *fromGenome = cgiOptionalString(argFromGenome);
 char *toGenome = cgiOptionalString(argToGenome);
 char *email = cgiOptionalString(argEmail);
 char *comment = cgiOptionalString(argComment);
 
 /* probably want a silent exit here */
 if (isEmpty(fromGenome) || isEmpty(toGenome) || isEmpty(email) || isEmpty(comment))
     apiErrAbort(err400, err400Msg, "must have all arguments: %s, %s, %s, %s for endpoint '/liftRequest", argFromGenome, argToGenome, argEmail, argComment);
 
 /* Require a session cookie.  Robots that have not
  *   passed the challenge will not have one. */
 char *cookieName = hUserCookie();
 char *userId = findCookieData(cookieName);
 if (isEmpty(userId))
     apiErrAbort(err400, err400Msg, "can not find required inputs for endpoint '/liftRequest");
 
 /* verify (again) that the requested assemblies actually exist */
 struct dbDb *fromDb = hDbDb(fromGenome);
 if (fromDb == NULL)
     {
     fromDb = genarkLiftOverDb(fromGenome);
     }
 struct dbDb *toDb = hDbDb(toGenome);
 if (toDb == NULL)
     {
     toDb = genarkLiftOverDb(toGenome);
     }
 if ( (fromDb == NULL) || (fromDb == NULL) )
     {
     if ( (fromDb == NULL) && (toDb == NULL) )
 	    apiErrAbort(err400, err400Msg, "can not find either 'fromGenome=%s' or 'toGenome=%s' for endpoint '/liftOver", fromGenome, toGenome);
         else
 	    apiErrAbort(err400, err400Msg, "can not find 'fromoGenome=%s' for endpoint '/liftOver", fromGenome);
     if (toDb == NULL)
         apiErrAbort(err400, err400Msg, "can not find 'toGenome=%s' for endpoint '/liftOver", toGenome);
     }
 
 /* duplicate-row guard: any existing row in ottoRequest for this pair
  * (either direction, any status) blocks resubmission.  The form's JS
  * already shows a "pending" panel for this case via the listExisting
  * endpoint; this is the backstop for clients that bypass the form. */
 {
 char *dupOttoTable = cfgOption("ottoTable");
 if (isNotEmpty(dupOttoTable))
     {
     struct sqlConnection *conn = hConnectOtto();
     if (sqlTableExists(conn, dupOttoTable))
         {
         struct dyString *dq = newDyString(0);
         sqlDyStringPrintf(dq,
             "SELECT COUNT(*) FROM %s WHERE requestType='liftOver' AND "
             "((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s'))",
             dupOttoTable, fromGenome, toGenome, toGenome, fromGenome);
         int dupCount = sqlQuickNum(conn, dyStringCannibalize(&dq));
         hDisconnectOtto(&conn);
         if (dupCount > 0)
             apiErrAbort(err409, err409Msg,
                 "A request for %s <-> %s has already been submitted "
                 "and is on record.  Duplicates are not accepted.",
                 fromGenome, toGenome);
         }
     else
         hDisconnectOtto(&conn);
     }
 }
 
 /* per-email daily rate limit, per requestType, calendar-day server time */
 char *limitStr = cfgOption("liftDailyLimit");
 int dailyLimit = isNotEmpty(limitStr) ? atoi(limitStr) : 0;
 if (dailyLimit > 0)
     {
     char *limitOttoTable = cfgOption("ottoTable");
     if (isNotEmpty(limitOttoTable))
         {
         struct sqlConnection *conn = hConnectOtto();
         if (sqlTableExists(conn, limitOttoTable))
             {
             struct dyString *q = newDyString(0);
             sqlDyStringPrintf(q,
                 "SELECT COUNT(*) FROM %s "
                 "WHERE requestType='liftOver' AND email='%s' "
                 "AND DATE(requestTime) = CURDATE()",
                 limitOttoTable, email);
             int todayCount = sqlQuickNum(conn, dyStringCannibalize(&q));
             hDisconnectOtto(&conn);
             if (todayCount >= dailyLimit)
                 apiErrAbort(err429, err429Msg,
                     "Daily limit reached: %d liftOver requests per day. "
                     " Please try again tomorrow.",
                     dailyLimit);
             }
         else
             hDisconnectOtto(&conn);
         }
     }
 
 char *toAddr = cfgOption("chainFileRequestEmail");
 char *fromAddr = cfgOption("apiFromEmail");
 
 if (isNotEmpty(toAddr) && isNotEmpty(fromAddr))
     {
     char nowTime[256];
     time_t seconds = clock1();
     struct tm *timeNow = localtime(&seconds);
     strftime(nowTime, sizeof nowTime, "%Y-%m-%d %H:%M:%S", timeNow);
 
     struct dyString *msg = newDyString(0);
     /* may need to encode these inputs to make them safe */
     dyStringPrintf(msg, "%s\nLift over request\nfrom: %s\nto: %s\nemail '%s'\ncomment: '%s'", nowTime, fromGenome, toGenome, email, comment);
 
     /* some kind of response here back to the request page */
     struct jsonWrite *jw = apiStartOutput();
     jsonWriteString(jw, "msg", dyStringCannibalize(&msg));
     apiFinishOutput(0,NULL,jw);
     char *ottoTable = cfgOption("ottoTable");	/* probably ottoRequest */
     if (isNotEmpty(ottoTable))
         {
         struct sqlConnection *conn = hConnectOtto();
         if (sqlTableExists(conn, ottoTable))
 	    {
             /* Atomic insert with duplicate check - prevents race condition */
             struct dyString *update = newDyString(0);
             sqlDyStringPrintf(update,
                 "INSERT INTO %s (requestType, fromDb, toDb, email, comment, requestTime, status, buildDir) "
                 "SELECT 'liftOver', '%s', '%s', '%s', '%s', now(), 0, '' "
                 "WHERE NOT EXISTS ("
                 "  SELECT 1 FROM %s WHERE requestType='liftOver' AND "
                 "  ((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s'))"
                 ")",
                 ottoTable, fromGenome, toGenome, email, comment,
                 ottoTable, fromGenome, toGenome, toGenome, fromGenome);
             int rowsAffected = sqlUpdateRows(conn, dyStringCannibalize(&update), NULL);
             if (rowsAffected == 0)
                 {
                 hDisconnectOtto(&conn);
                 apiErrAbort(err409, err409Msg,
                     "A request for %s <-> %s has already been submitted "
                     "and is on record.  Duplicates are not accepted.",
                     fromGenome, toGenome);
                 }
 	    }
         hDisconnectOtto(&conn);
         }
     }
 }	/*	void apiLiftRequest(char *words[MAX_PATH_INFO])	*/