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