3002a680b61eb4446e965da8e6f09a530ea9e3d6
max
  Fri Jan 26 16:20:56 2018 -0800
allow a second (recovery) email address on hgLogin, refs #20888

diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c
index 79fc5d4..457b537 100644
--- src/hg/hgLogin/hgLogin.c
+++ src/hg/hgLogin/hgLogin.c
@@ -13,30 +13,33 @@
 #include "htmshell.h"
 #include "cart.h"
 #include "hPrint.h"
 #include "hdb.h"
 #include "hui.h"
 #include "web.h"
 #include "ra.h"
 #include "hgColors.h"
 #include "net.h"
 #include "wikiLink.h"
 #include "hgLogin.h"
 #include "gbMembers.h"
 #include "versionInfo.h"
 #include "mailViaPipe.h"
 #include "dystring.h"
+#include "autoUpgrade.h"
+
+#define EMAILSEP ";"
 
 /* ---- Global variables. ---- */
 char msg[4096] = "";
 char *incorrectUsernameOrPassword="The username or password you entered is incorrect.";
 char *incorrectUsername="The username you entered is incorrect.";
 /* The excludeVars are not saved to the cart. */
 char *excludeVars[] = { "submit", "Submit", "debug", "fixMembers", "update", 
      "hgLogin_password", "hgLogin_password2", "hgLogin_newPassword1",
      "hgLogin_newPassword2", NULL };
 struct cart *cart;	/* This holds cgi and other variables between clicks. */
 char *database;		/* Name of genome database - hg15, mm3, or the like. */
 struct hash *oldCart;	/* Old cart hash. */
 char *errMsg;           /* Error message to show user when form data rejected */
 char brwName[64];
 char brwAddr[256];
@@ -334,30 +337,31 @@
     "\n"
     "<h2>%s</h2>", brwName);
 hPrintf(
     "<p id=\"confirmationMsg\" class=\"confirmationTxt\">A confirmation email has been sent to you. \n"
     "Please click the confirmation link in the email to activate your account.</p>"
     "\n"
     "<p><a href=\"%s\">Return</a></p>", returnURL);
 cartRemove(cart, "hgLogin_email");
 cartRemove(cart, "hgLogin_userName");
 }
 
 void sendActMailOut(char *email, char *subject, char *msg)
 /* send mail to email address */
 {
 int result;
+
 result = mailViaPipe(email, subject, msg, returnAddr);
 
 if (result == -1)
     {
     hPrintf(
         "<h2>%s</h2>", brwName);
     hPrintf(
         "<p align=\"left\">"
         "</p>"
         "<h3>Error emailing to: %s</h3>"
         "Click <a href=%s?hgLogin.do.displayAccHelpPage=1>here</a> to return.<br>",
         hgLoginUrl, email );
     }
 else
     redirectToLoginPage("hgLogin.do.displayActMailSuccess=1");
@@ -447,84 +451,88 @@
 
 safef(subject, sizeof(subject),"Your user name at the %s", brwName);
 safef(msg, sizeof(msg), 
     "  Someone (probably you, from IP address %s) has requested user name(s) associated with this email address at the %s: \n\n  %s\n\n%s\n%s", 
    remoteAddr, brwName, users, signature, returnAddr);
 sendMailOut(email, subject, msg);
 }
 
 void sendUsername(struct sqlConnection *conn, char *email)
 /* email user username(s)  */
 {
 struct sqlResult *sr;
 char **row;
 char query[256];
 
-/* find all the user names assocaited with this email address */
+/* find all the user names associated with this email address */
 char userList[512]="";
-sqlSafef(query,sizeof(query),"SELECT * FROM gbMembers WHERE email='%s'", email);
+sqlSafef(query,sizeof(query),"SELECT * FROM gbMembers WHERE email='%s' or recovEmail='%s'", email, email);
 sr = sqlGetResult(conn, query);
 int numUser = 0;
 while ((row = sqlNextRow(sr)) != NULL)
     {
     struct gbMembers *m = gbMembersLoad(row);
     if (numUser >= 1)
         safecat(userList, sizeof(userList), ", ");
     safecat(userList, sizeof(userList), m->userName);
     numUser += 1;
     }
 sqlFreeResult(&sr);
 mailUsername(email, userList);
 }
 
-void sendPwdMailOut(char *email, char *subject, char *msg, char *username)
+void sendPwdMailOut(char *email, char *recovEmail, char *subject, char *msg, char *username)
 /* send password reset mail to user at registered email address */
 {
 char *obj = cartUsualString(cart, "hgLogin_helpWith", "");
 int result;
+
 result = mailViaPipe(email, subject, msg, returnAddr);
+if ((result != -1) && !isEmpty(recovEmail))
+    result = mailViaPipe(recovEmail, subject, msg, returnAddr);
+
 if (result == -1)
     {
     hPrintf(
         "<h2>%s</h2>", brwName);
     hPrintf(
         "<p align=\"left\">"
         "</p>"
         "<h3>Error emailing %s to: %s</h3>"
         "Click <a href=%s?hgLogin.do.displayAccHelpPage=1>here</a> to return.<br>",
         hgLoginUrl, obj, email );
     }
 else
     {
     jsInlineF(
         "window.location = '%s?hgLogin.do.displayMailSuccessPwd=1&user=%s';\n"
         , hgLoginUrl, username);
     }
 }
 
-void sendNewPwdMail(char *username, char *email, char *password)
+void sendNewPwdMail(char *username, char *email, char *recovEmail, char *password)
 /* send user new password */
 {
 char subject[256];
 char msg[4096];
 char *remoteAddr=getenv("REMOTE_ADDR");
 
 safef(subject, sizeof(subject),"New temporary password for your account at the %s", brwName);
 safef(msg, sizeof(msg),
     "  Someone (probably you, from IP address %s) requested a new password for the %s (%s). A temporary password for user \"%s\" has been created and was set to \"%s\". If this was your intent, you will need to log in and choose a new password now. Your temporary password will expire in 7 days.\n\n  If someone else made this request, or if you have remembered your password, and you no longer wish to change it, you may ignore this message and continue using your old password.\n\n%s\n%s",
     remoteAddr, brwName, brwAddr, username, password, signature, returnAddr);
-sendPwdMailOut(email, subject, msg, username);
+sendPwdMailOut(email, recovEmail, subject, msg, username);
 }
 
 void displayAccHelpPage(struct sqlConnection *conn)
 /* draw the account help page */
 {
 char *email = cartUsualString(cart, "hgLogin_email", "");
 char *username = cartUsualString(cart, "hgLogin_userName", "");
 
 jsInline(
     "function toggle(value){\n"
     "if(value=='showE'){\n"
     " document.getElementById('usernameBox').style.display='none';\n"
     " document.getElementById('emailAddrBox').style.display='inline';\n"
     " } else {\n"
     " document.getElementById('usernameBox').style.display='inline';\n"
@@ -566,38 +574,43 @@
     "</div>"
     "</form>"
     "</div><!-- END - accountHelpBox -->", username, email, getReturnToURL());
 jsOnEventById("click", "password", "toggle('showU');");
 jsOnEventById("click", "username", "toggle('showE');");
 cartSaveSession(cart);
 }
 
 void sendNewPassword(struct sqlConnection *conn, char *username, char *password)
 /* email user new password  */
 {
 char query[256];
 /* find email address associated with this username */
 sqlSafef(query,sizeof(query),"SELECT email FROM gbMembers WHERE userName='%s'", username);
 char *email = sqlQuickString(conn, query);
+
 if (!email || sameString(email,""))
     {
     freez(&errMsg);
     errMsg = cloneString("Email address not found.");
     displayAccHelpPage(conn);
     return;
     }
-sendNewPwdMail(username, email, password);
+
+sqlSafef(query,sizeof(query),"SELECT recovEmail FROM gbMembers WHERE userName='%s'", username);
+char *recovEmail = sqlQuickString(conn, query);
+
+sendNewPwdMail(username, email, recovEmail, password);
 }
 
 void lostPassword(struct sqlConnection *conn, char *username)
 /* Generate and mail new password to user */
 {
 char query[256];
 char *password = generateRandomPassword();
 char encPwd[45] = "";
 encryptNewPwd(password, encPwd, sizeof(encPwd));
 sqlSafef(query,sizeof(query), "UPDATE gbMembers SET lastUse=NOW(),newPassword='%s', newPasswordExpire=DATE_ADD(NOW(), INTERVAL 7 DAY), passwordChangeRequired='Y' WHERE userName='%s'",
     encPwd, username);
 sqlUpdate(conn, query);
 sendNewPassword(conn, username, password);
 return;
 }
@@ -879,33 +892,41 @@
     "<span style='color:red;'>%s</span>"
     "\n", hgLoginUrl, errMsg ? errMsg : "");
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"userName\">Username</label>"
     "<input type=text name=\"hgLogin_userName\" value=\"%s\" size=\"30\" id=\"userName\">"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"emailAddr\">Email address</label>"
     "<input type=text name=\"hgLogin_email\" value=\"%s\" size=\"30\" id=\"emailAddr\">"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"reenterEmail\">Re-enter Email address</label>"
     "<input type=text name=\"hgLogin_email2\" value=\"%s\" size=\"30\" id=\"emailCheck\">"
-    "</div>"
-    "\n", cartUsualString(cart, "hgLogin_userName", ""), cartUsualString(cart, "hgLogin_email", ""),
+    "</div>\n",
+    cartUsualString(cart, "hgLogin_userName", ""), cartUsualString(cart, "hgLogin_email", ""),
     cartUsualString(cart, "hgLogin_email2", ""));
+
+if (sqlFieldIndex(conn, "gbMembers", "recovEmail") != -1)
+    hPrintf("<div class=\"inputGroup\">"
+        "<label for=\"recovEmail\">Recovery Email address e.g. personal email</label>"
+        "<input type=text name=\"hgLogin_recovEmail\" size=\"30\" id=\"recovEmail\">"
+        "</div>"
+        "\n");
+
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"password\">Password <small>(must be at least 5 characters)</small></label>"
     "<input type=password name=\"hgLogin_password\" value=\"%s\" size=\"30\" id=\"password\">"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"password\">Re-enter Password</label>"
     "<input type=password name=\"hgLogin_password2\" value=\"%s\" size=\"30\" id=\"passwordCheck\">"
     "\n"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "    <input type=\"submit\" name=\"hgLogin.do.signup\" value=\"Sign Up\" class=\"largeButton\"> &nbsp; "
     "    <a href=\"%s\">Cancel</a>"
     "</div>"
@@ -972,30 +993,39 @@
     {
     freez(&errMsg);
     errMsg = cloneString("Email cannot be blank.");
     signupPage(conn);
     return;
     }
 
 if (email && email2 && !sameString(email, email2))
     {
     freez(&errMsg);
     errMsg = cloneString("Email addresses do not match.");
     signupPage(conn);
     return;
     }
 
+char *recovEmail = cartUsualString(cart, "hgLogin_recovEmail", "");
+if (!isEmpty(recovEmail) && spc_email_isvalid(recovEmail) == 0)
+    {
+    freez(&errMsg);
+    errMsg = cloneString("Invalid format of the recovery email address.");
+    signupPage(conn);
+    return;
+    }
+
 password = cartUsualString(cart, "hgLogin_password", "");
 if (!password || sameString(password,"") || (strlen(password)<5))
     {
     freez(&errMsg);
     errMsg = cloneString("Password must be at least 5 characters long.");
     signupPage(conn);
     return;
     }
 
 char *password2 = cartUsualString(cart, "hgLogin_password2", "");
 if (!password2 || sameString(password2,"") )
     {
     freez(&errMsg);
     errMsg = cloneString("Password field cannot be blank.");
     signupPage(conn);
@@ -1009,30 +1039,34 @@
     return;
     }
 
 /* pass all the checks, OK to create the account now */
 char encPwd[45] = "";
 encryptNewPwd(password, encPwd, sizeof(encPwd));
 char *accActStatus = "N";
 
 if (sameWord(returnAddr, "NOEMAIL"))
     accActStatus = "Y";
 
 sqlSafef(query,sizeof(query), "INSERT INTO gbMembers SET "
     "userName='%s',realName='%s',password='%s',email='%s',"
     "lastUse=NOW(),accountActivated='%s'",
     user,user,encPwd,email,accActStatus);
+// set the recov email only if we got one (and we only got one if the table has this field)
+if (!isEmpty(recovEmail))
+    sqlSafefAppend(query, sizeof(query), ",recovEmail='%s'", recovEmail);
+
 sqlUpdate(conn, query);
 
 if (sameWord(returnAddr, "NOEMAIL"))
     {
     redirectToLoginPage("hgLogin.do.displayLoginPage=1");
     return;
     }
 
 setupNewAccount(conn, email, user);
 /* send out activate code mail, and display the mail confirmation box */
 hPrintf("<h2>%s</h2>", brwName);
 hPrintf(
     "<p align=\"left\">\n"
     "</p>\n"
     "<h3>User %s successfully added.</h3>\n", user);
@@ -1227,30 +1261,36 @@
 struct dyString *javascript = dyStringNew(1024);
 struct slName *newCookies = loginLogoutUser(), *sl;
 for (sl = newCookies;  sl != NULL;  sl = sl->next)
     dyStringPrintf(javascript, " document.cookie = '%s';", sl->name);
 jsInline(javascript->string);
 /* return to "returnto" URL */
 returnToURL(150);
 }
 
 void doMiddle(struct cart *theCart)
 /* Write the middle parts of the HTML page.
  * This routine sets up some globals and then
  * dispatches to the appropriate page-maker. */
 {
 struct sqlConnection *conn = hConnectCentral();
+
+// on mirrors, try to add the field 'recovEmail' to gbMembers. This may or may not work, depending on their config
+if (sqlFieldIndex(conn, "gbMembers", "recovEmail") == -1) {
+    autoUpgradeTableAddColumn(conn, "gbMembers", "recovEmail", "varchar(255)", FALSE, "''");
+}
+
 cart = theCart;
 safecpy(brwName,sizeof(brwName), browserName());
 safecpy(brwAddr,sizeof(brwAddr), browserAddr());
 safecpy(signature,sizeof(signature), mailSignature());
 safecpy(returnAddr,sizeof(returnAddr), mailReturnAddr());
 
 if (cartVarExists(cart, "hgLogin.do.changePasswordPage"))
     changePasswordPage(conn);
 else if (cartVarExists(cart, "hgLogin.do.changePassword"))
     changePassword(conn);
 else if (cartVarExists(cart, "hgLogin.do.displayAccHelpPage"))
     displayAccHelpPage(conn);
 else if (cartVarExists(cart, "hgLogin.do.accountHelp"))
     accountHelp(conn);
 else if (cartVarExists(cart, "hgLogin.do.activateAccount"))
@@ -1277,30 +1317,31 @@
 }
 
 void usage()
 /* Explain usage and exit. */
 {
 errAbort(
   "hgLogin - Stand alone CGI to handle Genome Browser login.\n"
   "usage:\n"
   "    hgLogin <various CGI settings>\n"
   );
 }
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
+
 long enteredMainTime = clock1000();
 pushCarefulMemHandler(100000000);
 cgiSpoof(&argc, argv);
 htmlSetStyleSheet("../style/userAccounts.css");
 htmlSetStyle(htmlStyleUndecoratedLink);
 htmlSetBgColor(HG_CL_OUTSIDE);
 htmlSetFormClass("accountScreen");
 
 struct dyString *dy;
 dy = dyStringCreate("%shgLogin", hLoginHostCgiBinUrl());
 hgLoginUrl = dyStringCannibalize(&dy);
 
 oldCart = hashNew(10);
 cartHtmlShell("Login - UCSC Genome Browser", doMiddle, hUserCookie(), excludeVars, oldCart);
 cgiExitTime("hgLogin", enteredMainTime);