e6a46ac87f958486b1d1bcef8e80dcd872ad81fe
hiram
  Sat Jun 10 19:01:23 2023 -0700
adding silent google reCAPTCHA v3 function to hgUserSuggestion refs #31325

diff --git src/hg/hgUserSuggestion/hgUserSuggestion.c src/hg/hgUserSuggestion/hgUserSuggestion.c
index 9ad04a6..2604eb8 100644
--- src/hg/hgUserSuggestion/hgUserSuggestion.c
+++ src/hg/hgUserSuggestion/hgUserSuggestion.c
@@ -8,30 +8,33 @@
 #include "hCommon.h"
 #include "jksql.h"
 #include "portable.h"
 #include "cheapcgi.h"
 #include "htmshell.h"
 #include "hdb.h"
 #include "hui.h"
 #include "cart.h"
 #include "hPrint.h"
 #include "dbDb.h"
 #include "web.h"
 #include "hash.h"
 #include "hgConfig.h"
 #include "hgUserSuggestion.h"
 #include "mailViaPipe.h"
+#include "htmlPage.h"
+#include "net.h"
+#include "jsonParse.h"
 
 /* ---- Global variables. ---- */
 struct cart *cart;	/* The user's ui state. */
 struct hash *oldVars = NULL;
 
 /* ---- Global helper functions ---- */
 void checkHgConfForSuggestion()
 /* Abort if hg.conf has not been set up to accept suggestion */
 {
 if (isEmpty(cfgOption(CFG_SUGGEST_MAILTOADDR)) ||
     isEmpty(cfgOption(CFG_SUGGEST_MAILFROMADDR)) ||
     isEmpty(cfgOption(CFG_FILTERKEYWORD))        ||
     isEmpty(cfgOption(CFG_SUGGEST_MAIL_SIGNATURE)) ||
     isEmpty(cfgOption(CFG_SUGGEST_MAIL_RETURN_ADDR)) ||
     isEmpty(cfgOption(CFG_SUGGEST_BROWSER_NAME)))
@@ -173,45 +176,57 @@
     "       <label for=\"confirmEmail\">Re-enter Your Email:</label><input type=\"text\" \n"
     "          name=\"suggestCfmEmail\" id=\"cfmemail\" size=\"50\" style=\"margin-left:20px\" maxlength=\"254\"/><BR><BR>\n");
 hPrintf(
     "       <label for=\"category\">Category:</label><select name=\"suggestCategory\" id=\"category\" style=\"margin-left:20px\" maxlength=\"256\">\n"
     "         <option selected>Tracks</option> \n"
     "         <option>Genome Assemblies</option>\n"
     "         <option>Browser Tools</option>\n"
     "         <option>Command-line Utilities</option>\n"
     "         <option>Others</option>\n"
     "         </select><BR><BR>\n");
 hPrintf(
     "       <label for=\"summary\">Summary:</label><input type=\"text\" name=\"suggestSummary\" id=\"summary\" size=\"74\" style=\"margin-left:20px\" maxlength=\"256\"/><BR><BR>\n"
     "       <label for=\"details\">Details:</label><BR><textarea name=\"suggestDetails\" id=\"details\" cols=\"100\" rows=\"15\" maxlength=\"4096\"></textarea><BR><BR>\n"
     "<input type=\"text\" name=\"suggestWebsite\" style=\"display: none;\" />"
     "     </div>\n");
+if (isNotEmpty(cfgOption(CFG_SUGGEST_SITE_KEY)))
+    {
+    // hidden reCaptcha token input
+    hPrintf("<input type='hidden' id='reCaptchaToken' name='reCaptchaToken'>");
+    }
+else
+    {
     hPrintf(
     "         <p>\n"
     "           <label for=\"code\">Enter the following value below: <span id=\"txtCaptchaDiv\" style=\"color:#F00\"></span><BR> \n"
     "           <input type=\"hidden\" id=\"txtCaptcha\" /></label>\n"
     "           <input type=\"text\" name=\"txtInput\" id=\"txtInput\" size=\"30\" />\n"
     "         </p>\n");
+    }
+
 hPrintf(
     "      <div class=\"formControls\">\n"
     "        <input id=\"sendButton\" type=\"button\" value=\"Send\"> \n"
     "        <input type=\"reset\" name=\"suggestClear\" value=\"Clear\" class=\"largeButton\"> \n"
     "      </div>\n"
     "      \n"
     "     </FORM>\n\n");
+
+
 jsOnEventById("click","sendButton","submitform();");
 }
+
 void printValidateScript()
 /* javascript to validate form inputs */
 {
 jsInline(
     "    function validateMainForm(theform)\n"
     "    {\n"
     "    var x=theform.suggestName.value;\n"
     "    if (x==null || x==\"\")\n"
     "      {\n"
     "      alert(\"Name field must be filled out\");\n"
     "      theform.suggestName.focus() ;\n"
     "      return false;\n"
     "      }\n"
     "    var y=theform.suggestEmail.value;\n"
     "    if (y==null || y==\"\")\n"
@@ -299,69 +314,103 @@
     "         if (str1 == str2){\n"
     "                 return true;\n"
     "         } else {\n"
     "                 return false;\n"
     "         }\n"
     " }\n\n"
     " function removeSpaces(string){\n"
     "         return string.split(' ').join('');\n"
     " }\n"
     );
 }
 
 void printSubmitFormScript()
 /* javascript to submit form */
 {
+if (isNotEmpty(cfgOption(CFG_SUGGEST_SITE_KEY)))
+    {
+    struct dyString *jsText = dyStringNew(0);
+    dyStringPrintf(jsText, "\n function submitform(){\n"
+    "         if ( validateMainForm(document.forms['mainForm'])){\n"
+    "             grecaptcha.execute('%s', { action: 'userSuggest' })\n"
+    "               .then(function(token) {\n"
+    "                document.getElementById('reCaptchaToken').value = token;\n"
+    "                 document.forms['mainForm'].submit();\n"
+    "             });\n"
+    "         }\n"
+    " }\n", cfgOption(CFG_SUGGEST_SITE_KEY));
+    jsInline(dyStringCannibalize(&jsText));
+    }
+else
+    {
 jsInline(
-    "     function submitform()\n"
-    "     {\n"
-    "      if ( validateMainForm(document.forms[\"mainForm\"]) && checkCaptcha(document.forms[\"mainForm\"]))\n"
-    "        {\n"
-    "          document.forms[\"mainForm\"].submit();\n"
+    "\n function submitform(){\n"
+    "         if ( validateMainForm(document.forms['mainForm']) && checkCaptcha(document.forms['mainForm'])){\n"
+    "                 document.forms['mainForm'].submit();\n"
     "         }\n"
     " }\n"
     );
     }
+}
 
-void printSuggestionConfirmed(char *summary, char * refID, char *userAddr, char *adminAddr, char *details)
+static void printReCaptchaV3()
+/* output the js to perform the reCAPTCHA v3 function */
+{
+jsInline(
+    "\n window.onload = function() {\n"
+    "  grecaptcha.ready();\n"
+    " };\n"
+    );
+}
+
+void printSuggestionConfirmed(char *summary, char * refID, char *userAddr, char *adminAddr, char *details, double captchaScore)
 /* display suggestion confirm page */
 {
 hPrintf(
     "<h2>Thank you for your suggestion!</h2>");
 hPrintf(
     "<p>"
     "You may follow up on the status of your request at any time by "
     "<a href=\"../contacts.html#followup\">contacting us</a> and quoting your reference number:<BR><BR>%s<BR><BR>"
     "A copy of this information has also been sent to you at %s.<BR></p>",
      refID, userAddr);
 hPrintf(
     "<p><a href=\"hgUserSuggestion\">Click here if you wish to make additional suggestions.</a></p>");
 hPrintf(
     "<p>"
     "<B>Your suggestion summary:</B><BR>"
     "%s<BR>"
     "<B>Your suggestion details:</B><BR>"
     "<pre>%s</pre>"
     "</p>",
     summary, details);
+if (captchaScore > -1.0)
+    hPrintf("<p>(google captcha score: %g)</p>\n", captchaScore);
 }
 
-void printInvalidForm()
+void printInvalidForm(double captchaScore)
 /* display invalid form page */
 {
 hPrintf(
     "<h2>Invalid Form.</h2>");
+if (captchaScore > -1.0)
+    hPrintf(
+	"<p>"
+	"The form is invalid. Please correct it and "
+	"<a id='goBack' >submit</a> again. (score: %g)</p>", captchaScore
+    );
+else
     hPrintf(
 	"<p>"
 	"The form is invalid. Please correct it and "
 	"<a id='goBack' >submit</a> again.</p>"
     );
 jsOnEventById("click", "goBack", "history.go(-1)");
 }
 
 void printInvalidCategory(char *invalidCategory)
 /* display invalid category page */
 {
 hPrintf(
     "<h2>Invalid Category.</h2>");
 hPrintf(
     "<p>"
@@ -417,99 +466,196 @@
 safecpy(signature,sizeof(signature), mailSignature());
 safecpy(userEmailAddr, sizeof(userEmailAddr),emailAddr);
 safef(subject, sizeof(subject),"Thank you for your suggestion to the %s", brwName);
 safef(msg, sizeof(msg),
     "  Someone (probably you, from IP address %s) submitted a suggestion to the %s regarding \"%s\".\n\n  The suggestion has been assigned a reference number of \"%s\". If you wish to follow up on the progress of this suggestion with browser staff, you may contact us at %s. Please include the reference number of your suggestion in the email.\n\nThank you for your input,\n%s\n\nYour suggestion summary:\n%s\n\nYour suggestion details:\n%s",
 remoteAddr, brwName, summary, suggestID, returnAddr, signature, summary, details);
 // ignore returned result
 mailViaPipe(userEmailAddr, subject, msg, returnAddr);
 }
 
 void askForSuggest(char *organism, char *db)
 /* Put up the suggestion form. */
 {
 printMainForm();
 printValidateScript();
+if (isNotEmpty(cfgOption(CFG_SUGGEST_SITE_KEY)))
+    {
+    printSubmitFormScript();
+    }
+else
+    {
     printCheckCaptchaScript();
     printSubmitFormScript();
+    }
 //cartSaveSession(cart);
 }
 
+static void recordError(double score, char *msg, char *urlResult)
+/* record captcha errors in apache error_log */
+{
+if (urlResult)
+    {
+    stripChar(urlResult, '\n');
+    stripChar(urlResult, ' ');
+    fprintf(stderr, "reCAPTCHA score: %g, '%s' '%s'\n", score, msg, urlResult);
+    }
+else
+    fprintf(stderr, "reCAPTCHA score: %g, '%s'\n", score, msg);
+}
+
 void  submitSuggestion()
 /* send the suggestion to ,.. */
 {
 /* parameters from hg.conf */
 
 /* values from cart */
 char *sName=cartUsualString(cart,"suggestName","");
+boolean captchaRobot = FALSE;	// OK when not configured
+double captchaScore = -1.0;
+
+if (isNotEmpty(cfgOption(CFG_SUGGEST_SECRET_KEY)))
+    {
+    captchaRobot = TRUE;	// assume robot until proven human
+    captchaScore = -0.9;	// and allow score to show up in printout
+  char *threshHoldString = cfgOptionDefault(CFG_SUGGEST_HUMAN_THRESHOLD, "0.5");
+    double threshHoldScore = sqlDouble(threshHoldString);
+    char *reCaptcha = cartUsualString(cart,"reCaptchaToken", NULL);
+    if (reCaptcha)
+        {
+        char siteverify[4096];
+        safef(siteverify, sizeof(siteverify), "https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s", cfgOption(CFG_SUGGEST_SECRET_KEY), reCaptcha);
+        struct htmlPage *verify = htmlPageGet(siteverify);
+    /*  successful return:
+
+    {  "success": true,  "challenge_ts": "2023-06-09T23:47:35Z",  "hostname": "genome-test.gi.ucsc.edu",  "score": 0.9,  "action": "userSuggest"}
+
+        error return:
+    {  "success": false,  "error-codes": [    "timeout-or-duplicate"  ]}'
+    */
+
+        if (verify)
+            {
+            struct lm *lm = lmInit(1<<16);
+            struct jsonElement *jsonObj = jsonParseLm(verify->htmlText, lm);
+            if (jsonObj)
+                {
+                struct jsonElement *score = jsonFindNamedField(jsonObj, NULL, "score");
+                struct jsonElement *success = jsonFindNamedField(jsonObj, NULL, "success");
+                if (success)
+		    {
+		    if (success->val.jeBoolean)
+			{
+			if (score)
+			    {
+                            captchaScore = score->val.jeDouble;
+			    if (score->val.jeDouble > threshHoldScore)
+				{
+				captchaRobot = FALSE;
+				recordError(captchaScore, "captcha approved", verify->htmlText);
+				}
+			    else
+				recordError(captchaScore, "score < threshold", verify->htmlText);
+			    }
+			else
+			    recordError(captchaScore, "score not found in JSON", verify->htmlText);
+			}	// if (success->val.jeBoolean)
+		    else
+			recordError(captchaScore, "success FALSE", verify->htmlText);
+		    }	// if (success)
+		else
+		  recordError(captchaScore, "success status not found in JSON", verify->htmlText);
+                }	// if (jsonObj)
+	    else
+		recordError(captchaScore, "JSON object not found in siteverify return", verify->htmlText);
+            }	// if (verify)
+	else
+	    recordError(captchaScore, "siteverify URL access failed", NULL);
+        }	// if (reCaptcha)
+    else
+	recordError(captchaScore, "reCaptchaToken not found in form", NULL);
+    }	// if (isNotEmpty(cfgOption(CFG_SUGGEST_SECRET_KEY)))
+
 char *sEmail=cartUsualString(cart,"suggestEmail","");
 char *sCategory=cartUsualString(cart,"suggestCategory","");
 char *sSummary=cartUsualString(cart,"suggestSummary","");
 char *sDetails=cartUsualString(cart,"suggestDetails","");
 char *sWebsite=cartUsualString(cart,"suggestWebsite","");
 char suggestID[512];
 safef(suggestID, sizeof(suggestID),"%s %s", sEmail, now());
 
 /* reject if the hidden field is not blank */
-if (isNotEmpty(sWebsite))
+if (isNotEmpty(sWebsite) || captchaRobot)
     {
-    printInvalidForm();
+    printInvalidForm(captchaScore);
     cartSetString(cart, "suggestWebsite", "");
     return;
     }
 
 /* reject suggestion if category is invalid */
 if (!validateCategory(sCategory))
     {
     printInvalidCategory(sCategory);
     return;
     }
 
 /* Send back suggestion only with valid user email address */
 if (spc_email_isvalid(sEmail) != 0)
     {
     /* send back the suggestion */
     sendSuggestionBack(sName, sEmail, sCategory, sSummary, sDetails, suggestID);
     /* send confirmation mail to user */
     sendConfirmMail(sEmail,suggestID, sSummary, sDetails);
     /* display confirmation page */
-    printSuggestionConfirmed(sSummary, suggestID, sEmail, mailReturnAddr(), sDetails);
+    printSuggestionConfirmed(sSummary, suggestID, sEmail, mailReturnAddr(), sDetails, captchaScore);
     }
 else
     {
     /* save all field value in cart */
      printInvalidEmailAddr(sEmail);
     }
 cartRemove(cart, "do.suggestSendMail");
 }
 
 void doMiddle(struct cart *theCart)
 /* Write header and body of html page. */
 {
 char *db, *organism;
 cart = theCart;
 getDbAndGenome(cart, &db, &organism, oldVars);
 cartWebStart(theCart, db, "UCSC Genome Browser: Suggestion Box");
 checkHgConfForSuggestion();
 if (cartVarExists(cart, "do.suggestSendMail"))
     {
     submitSuggestion();
     cartRemove(cart, "do.suggestSendMail");
     return;
     }
 
 askForSuggest(organism,db);
+if (isNotEmpty(cfgOption(CFG_SUGGEST_SITE_KEY)))
+    {
+    printReCaptchaV3();
+    hPrintf("<div><p>this site protected by reCAPTCHA and the Google <a href='https://policies.google.com/privacy?hl=en' target='_blank'>privacy policy</a> and <a href='https://policies.google.com/terms?hl=en' target='_blank'>terms of service</a> apply</p></div>\n");
+
+    char footer[1024];
+    safef(footer, sizeof(footer),
+   "<script src='https://www.google.com/recaptcha/api.js?render=%s'></script>",
+           cfgOption(CFG_SUGGEST_SITE_KEY));
+    cartWebEndExtra(footer);
+    }
+else
     cartWebEnd();
 }
 
 /* Null terminated list of CGI Variables we don't want to save
  * permanently. */
 char *excludeVars[] = {"Submit", "submit", "Clear", NULL};
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 long enteredMainTime = clock1000();
 oldVars = hashNew(10);
 cgiSpoof(&argc, argv);
 cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars);
 cgiExitTime("hgUserSuggestion", enteredMainTime);