/**
* =============================================================================
*
* ORCID (R) Open Source
* http://orcid.org
*
* Copyright (c) 2012-2014 ORCID, Inc.
* Licensed under an MIT-Style License (MIT)
* http://orcid.org/open-source-license
*
* This copyright and license information (including a link to the full license)
* shall be included in its entirety in all copies or substantial portion of
* the software.
*
* =============================================================================
*/
package org.orcid.frontend.web.controllers;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.regex.Pattern;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.orcid.core.constants.EmailConstants;
import org.orcid.core.manager.EncryptionManager;
import org.orcid.core.manager.InternalSSOManager;
import org.orcid.core.manager.NotificationManager;
import org.orcid.core.manager.OrcidSearchManager;
import org.orcid.core.manager.ProfileEntityManager;
import org.orcid.core.manager.RegistrationManager;
import org.orcid.frontend.spring.ShibbolethAjaxAuthenticationSuccessHandler;
import org.orcid.frontend.spring.SocialAjaxAuthenticationSuccessHandler;
import org.orcid.frontend.spring.web.social.config.SocialContext;
import org.orcid.frontend.web.controllers.helper.SearchOrcidSolrCriteria;
import org.orcid.frontend.web.util.RecaptchaVerifier;
import org.orcid.jaxb.model.message.FamilyName;
import org.orcid.jaxb.model.message.OrcidIdentifier;
import org.orcid.jaxb.model.message.OrcidMessage;
import org.orcid.jaxb.model.message.OrcidProfile;
import org.orcid.jaxb.model.message.OrcidSearchResult;
import org.orcid.jaxb.model.message.SendEmailFrequency;
import org.orcid.persistence.jpa.entities.EmailEntity;
import org.orcid.pojo.DupicateResearcher;
import org.orcid.pojo.Redirect;
import org.orcid.pojo.ajaxForm.PojoUtil;
import org.orcid.pojo.ajaxForm.Registration;
import org.orcid.pojo.ajaxForm.RequestInfoForm;
import org.orcid.pojo.ajaxForm.Text;
import org.orcid.utils.OrcidRequestUtil;
import org.orcid.utils.OrcidStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Controller;
import org.springframework.validation.FieldError;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;
/**
* @author Will Simpson
*/
@Controller
public class RegistrationController extends BaseController {
public static Pattern givenNamesPattern = Pattern.compile("given_names=([^&]*)");
public static Pattern familyNamesPattern = Pattern.compile("family_names=([^&]*)");
public static Pattern emailPattern = Pattern.compile("email=([^&]*)");
private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationController.class);
final static Integer DUP_SEARCH_START = 0;
final static Integer DUP_SEARCH_ROWS = 25;
public final static String GRECAPTCHA_SESSION_ATTRIBUTE_NAME = "verified-recaptcha";
private static Random rand = new Random();
@Resource
private RegistrationManager registrationManager;
@Resource
private AuthenticationManager authenticationManager;
@Resource
private OrcidSearchManager orcidSearchManager;
@Resource
private EncryptionManager encryptionManager;
@Resource
private NotificationManager notificationManager;
@Resource
private RecaptchaVerifier recaptchaVerifier;
@Resource
private InternalSSOManager internalSSOManager;
@Autowired
private SocialContext socialContext;
@Resource
private SocialAjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandlerSocial;
@Resource
private ShibbolethAjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandlerShibboleth;
@Resource
private ProfileEntityManager profileEntityManager;
@RequestMapping(value = "/register.json", method = RequestMethod.GET)
public @ResponseBody Registration getRegister(HttpServletRequest request, HttpServletResponse response) {
// Remove the session hash if needed
if (request.getSession().getAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME) != null) {
request.getSession().removeAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME);
}
Registration reg = new Registration();
reg.getEmail().setRequired(true);
reg.getEmailConfirm().setRequired(true);
reg.getFamilyNames().setRequired(false);
reg.getGivenNames().setRequired(true);
reg.getSendChangeNotifications().setValue(true);
reg.getSendOrcidNews().setValue(true);
reg.getSendMemberUpdateRequests().setValue(true);
reg.getSendEmailFrequencyDays().setValue(SendEmailFrequency.WEEKLY.value());
reg.getTermsOfUse().setValue(false);
setError(reg.getTermsOfUse(), "validations.acceptTermsAndConditions");
RequestInfoForm requestInfoForm = (RequestInfoForm) request.getSession().getAttribute(OauthControllerBase.REQUEST_INFO_FORM);
if (requestInfoForm != null) {
if (!PojoUtil.isEmpty(requestInfoForm.getUserEmail())) {
reg.getEmail().setValue(requestInfoForm.getUserEmail());
}
if (!PojoUtil.isEmpty(requestInfoForm.getUserGivenNames())) {
reg.getGivenNames().setValue(requestInfoForm.getUserGivenNames());
}
if (!PojoUtil.isEmpty(requestInfoForm.getUserFamilyNames())) {
reg.getFamilyNames().setValue(requestInfoForm.getUserFamilyNames());
}
}
long numVal = generateRandomNumForValidation();
reg.setValNumServer(numVal);
reg.setValNumClient(0);
return reg;
}
public long generateRandomNumForValidation() {
int numCheck = rand.nextInt(1000000);
if (numCheck % 2 != 0)
numCheck += 1;
return numCheck;
}
@RequestMapping(value = "/register.json", method = RequestMethod.POST)
public @ResponseBody Registration setRegister(HttpServletRequest request, @RequestBody Registration reg) {
validateRegistrationFields(request, reg);
validateGrcaptcha(request, reg);
return reg;
}
public void validateGrcaptcha(HttpServletRequest request, @RequestBody Registration reg) {
// If recatcha wasn't loaded do nothing. This is for countries that
// block google.
if (reg.getGrecaptchaWidgetId().getValue() != null) {
if (reg.getGrecaptcha() == null) {
reg.setGrecaptcha(new Text());
reg.getGrecaptcha().setErrors(new ArrayList<String>());
setError(reg.getGrecaptcha(), "registrationForm.recaptcha.error");
setError(reg, "registrationForm.recaptcha.error");
} else {
reg.getGrecaptcha().setErrors(new ArrayList<String>());
}
if (request.getSession().getAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME) != null) {
if (!reg.getGrecaptcha().getValue().equals(request.getSession().getAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME))) {
setError(reg.getGrecaptcha(), "registrationForm.recaptcha.error");
setError(reg, "registrationForm.recaptcha.error");
}
} else if (!recaptchaVerifier.verify(reg.getGrecaptcha().getValue())) {
reg.getGrecaptcha().setErrors(new ArrayList<String>());
setError(reg.getGrecaptcha(), "registrationForm.recaptcha.error");
setError(reg, "registrationForm.recaptcha.error");
} else {
request.getSession().setAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME, reg.getGrecaptcha().getValue());
}
}
}
@RequestMapping(value = { "/registerConfirm.json", "/shibboleth/registerConfirm.json" }, method = RequestMethod.POST)
public @ResponseBody Redirect setRegisterConfirm(HttpServletRequest request, HttpServletResponse response, @RequestBody Registration reg)
throws UnsupportedEncodingException {
Redirect r = new Redirect();
boolean usedCaptcha = false;
// If recatcha wasn't loaded do nothing. This is for countries that
// block google.
if (reg.getGrecaptchaWidgetId().getValue() != null) {
// If the captcha verified key is not in the session, redirect to
// the login page
if (request.getSession().getAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME) == null || PojoUtil.isEmpty(reg.getGrecaptcha())
|| !reg.getGrecaptcha().getValue().equals(request.getSession().getAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME))) {
r.setUrl(getBaseUri() + "/register");
return r;
}
usedCaptcha = true;
}
// Remove the session hash if needed
if (request.getSession().getAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME) != null) {
request.getSession().removeAttribute(GRECAPTCHA_SESSION_ATTRIBUTE_NAME);
}
// Strip any html code from names before validating them
if (!PojoUtil.isEmpty(reg.getFamilyNames())) {
reg.getFamilyNames().setValue(OrcidStringUtils.stripHtml(reg.getFamilyNames().getValue()));
}
if (!PojoUtil.isEmpty(reg.getGivenNames())) {
reg.getGivenNames().setValue(OrcidStringUtils.stripHtml(reg.getGivenNames().getValue()));
}
// make sure validation still passes
validateRegistrationFields(request, reg);
if (reg.getErrors() != null && reg.getErrors().size() > 0) {
r.getErrors().add("Please revalidate at /register.json");
return r;
}
if (reg.getValNumServer() == 0 || reg.getValNumClient() != reg.getValNumServer() / 2) {
r.setUrl(getBaseUri() + "/register");
return r;
}
try {
// Locale
Locale locale = RequestContextUtils.getLocale(request);
// Ip
String ip = OrcidRequestUtil.getIpAddress(request);
createMinimalRegistrationAndLogUserIn(request, response, reg, usedCaptcha, locale, ip);
} catch(Exception e) {
r.getErrors().add(getMessage("register.error.generalError"));
return r;
}
if ("social".equals(reg.getLinkType()) && socialContext.isSignedIn(request, response) != null) {
ajaxAuthenticationSuccessHandlerSocial.linkSocialAccount(request, response);
} else if ("shibboleth".equals(reg.getLinkType())) {
ajaxAuthenticationSuccessHandlerShibboleth.linkShibbolethAccount(request, response);
}
String redirectUrl = calculateRedirectUrl(request, response);
r.setUrl(redirectUrl);
return r;
}
public void validateRegistrationFields(HttpServletRequest request, Registration reg) {
reg.setErrors(new ArrayList<String>());
registerGivenNameValidate(reg);
registerPasswordValidate(reg);
registerPasswordConfirmValidate(reg);
regEmailValidate(request, reg, false, false);
registerTermsOfUseValidate(reg);
copyErrors(reg.getEmailConfirm(), reg);
copyErrors(reg.getEmail(), reg);
copyErrors(reg.getGivenNames(), reg);
copyErrors(reg.getPassword(), reg);
copyErrors(reg.getPasswordConfirm(), reg);
copyErrors(reg.getTermsOfUse(), reg);
}
@RequestMapping(value = "/registerPasswordConfirmValidate.json", method = RequestMethod.POST)
public @ResponseBody Registration registerPasswordConfirmValidate(@RequestBody Registration reg) {
passwordConfirmValidate(reg.getPasswordConfirm(), reg.getPassword());
return reg;
}
@RequestMapping(value = "/registerPasswordValidate.json", method = RequestMethod.POST)
public @ResponseBody Registration registerPasswordValidate(@RequestBody Registration reg) {
passwordValidate(reg.getPasswordConfirm(), reg.getPassword());
return reg;
}
@RequestMapping(value = "/registerTermsOfUseValidate.json", method = RequestMethod.POST)
public @ResponseBody Registration registerTermsOfUseValidate(@RequestBody Registration reg) {
termsOfUserValidate(reg.getTermsOfUse());
return reg;
}
@RequestMapping(value = "/registerGivenNamesValidate.json", method = RequestMethod.POST)
public @ResponseBody Registration registerGivenNameValidate(@RequestBody Registration reg) {
super.givenNameValidate(reg.getGivenNames());
return reg;
}
@RequestMapping(value = "/registerEmailValidate.json", method = RequestMethod.POST)
public @ResponseBody Registration regEmailValidate(HttpServletRequest request, @RequestBody Registration reg) {
return regEmailValidate(request, reg, false, true);
}
public Registration regEmailValidate(HttpServletRequest request, Registration reg, boolean isOauthRequest, boolean isKeyup) {
reg.getEmail().setErrors(new ArrayList<String>());
if (!isKeyup && (reg.getEmail().getValue() == null || reg.getEmail().getValue().trim().isEmpty())) {
setError(reg.getEmail(), "Email.registrationForm.email");
}
String emailAddress = reg.getEmail().getValue();
MapBindingResult mbr = new MapBindingResult(new HashMap<String, String>(), "Email");
// Validate the email address is ok
if(!validateEmailAddress(emailAddress)) {
String[] codes = { "Email.personalInfoForm.email" };
String[] args = { emailAddress };
mbr.addError(new FieldError("email", "email", emailAddress, false, codes, args, "Not vaild"));
} else {
//Validate duplicates
//If email exists
if(emailManager.emailExists(emailAddress)) {
String orcid = emailManager.findOrcidIdByEmail(emailAddress);
String[] args = { emailAddress };
//If it is claimed, should return a duplicated exception
if(profileEntityManager.isProfileClaimedByEmail(emailAddress)) {
String[] codes = null;
if(profileEntityManager.isDeactivated(orcid)) {
codes = new String[] { "orcid.frontend.verify.deactivated_email" };
} else {
codes = new String[] { "orcid.frontend.verify.duplicate_email" };
}
mbr.addError(new FieldError("email", "email", emailAddress, false, codes, args, "Email already exists"));
} else {
if(profileEntityManager.isDeactivated(orcid)) {
String[] codes = new String[] { "orcid.frontend.verify.deactivated_email" };
mbr.addError(new FieldError("email", "email", emailAddress, false, codes, args, "Email already exists"));
} else if(!emailManager.isAutoDeprecateEnableForEmail(emailAddress)) {
//If the email is not eligible for auto deprecate, we should show an email duplicated exception
String resendUrl = createResendClaimUrl(emailAddress, request);
String[] codes = { "orcid.frontend.verify.unclaimed_email" };
args = new String[] { emailAddress, resendUrl };
mbr.addError(new FieldError("email", "email", emailAddress, false, codes, args, "Unclaimed record exists"));
} else {
LOGGER.info("Email " + emailAddress + " belongs to a unclaimed record and can be auto deprecated");
}
}
}
}
for (ObjectError oe : mbr.getAllErrors()) {
Object[] arguments = oe.getArguments();
if (isOauthRequest && oe.getCode().equals("orcid.frontend.verify.duplicate_email")) {
// XXX
reg.getEmail().getErrors().add(getMessage("oauth.registration.duplicate_email", arguments));
} else if (oe.getCode().equals("orcid.frontend.verify.duplicate_email")) {
Object email = "";
if (arguments != null && arguments.length > 0) {
email = arguments[0];
}
String link = "/signin";
String linkType = reg.getLinkType();
if ("social".equals(linkType)) {
link = "/social/access";
} else if ("shibboleth".equals(linkType)) {
link = "/shibboleth/signin";
}
reg.getEmail().getErrors().add(getMessage(oe.getCode(), email, orcidUrlManager.getBaseUrl() + link));
}
else if(oe.getCode().equals("orcid.frontend.verify.deactivated_email")){
// Handle this message in angular to allow AJAX action
reg.getEmail().getErrors().add(oe.getCode());
}
else {
reg.getEmail().getErrors().add(getMessage(oe.getCode(), oe.getArguments()));
}
}
// validate confirm if already field out
if (reg.getEmailConfirm().getValue() != null) {
regEmailConfirmValidate(reg);
}
return reg;
}
@RequestMapping(value = "/registerEmailConfirmValidate.json", method = RequestMethod.POST)
public @ResponseBody Registration regEmailConfirmValidate(@RequestBody Registration reg) {
reg.getEmailConfirm().setErrors(new ArrayList<String>());
// normalize to "" sometimes angular sends null
if (reg.getEmail().getValue() == null)
reg.getEmail().setValue("");
if (reg.getEmailConfirm().getValue() == null)
reg.getEmailConfirm().setValue("");
if (!reg.getEmailConfirm().getValue().equalsIgnoreCase(reg.getEmail().getValue())) {
setError(reg.getEmailConfirm(), "StringMatchIgnoreCase.registrationForm");
}
return reg;
}
@RequestMapping(value = "/register", method = RequestMethod.GET)
public ModelAndView register(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mav = new ModelAndView("register");
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
LOGGER.debug("Saved url before registration is: " + (savedRequest != null ? savedRequest.getRedirectUrl() : " no saved request"));
return mav;
}
@RequestMapping(value = "/dupicateResearcher.json", method = RequestMethod.GET)
public @ResponseBody List<DupicateResearcher> getDupicateResearcher(@RequestParam("givenNames") String givenNames, @RequestParam("familyNames") String familyNames) {
List<DupicateResearcher> drList = new ArrayList<DupicateResearcher>();
List<OrcidProfile> potentialDuplicates = findPotentialDuplicatesByFirstNameLastName(givenNames, familyNames);
for (OrcidProfile op : potentialDuplicates) {
DupicateResearcher dr = new DupicateResearcher();
if (op.getOrcidBio() != null) {
if (op.getOrcidBio().getContactDetails() != null) {
if (op.getOrcidBio().getContactDetails().retrievePrimaryEmail() != null) {
dr.setEmail(op.getOrcidBio().getContactDetails().retrievePrimaryEmail().getValue());
}
}
FamilyName familyName = op.getOrcidBio().getPersonalDetails().getFamilyName();
if (familyName != null) {
dr.setFamilyNames(familyName.getContent());
}
dr.setGivenNames(op.getOrcidBio().getPersonalDetails().getGivenNames().getContent());
dr.setInstitution(null);
}
OrcidIdentifier orcidIdentifier = op.getOrcidIdentifier();
// Everything should be reindexed with orcid-identifier by now, but
// check for null just in case.
if (orcidIdentifier != null) {
dr.setOrcid(orcidIdentifier.getPath());
}
drList.add(dr);
}
return drList;
}
@RequestMapping(value = "/verify-email/{encryptedEmail}", method = RequestMethod.GET)
public ModelAndView verifyEmail(HttpServletRequest request, @PathVariable("encryptedEmail") String encryptedEmail, RedirectAttributes redirectAttributes)
throws NoSuchRequestHandlingMethodException, UnsupportedEncodingException {
try {
String decryptedEmail = encryptionManager.decryptForExternalUse(new String(Base64.decodeBase64(encryptedEmail), "UTF-8"));
EmailEntity emailEntity = emailManager.find(decryptedEmail);
String emailOrcid = emailEntity.getProfile().getId();
if (!getCurrentUserOrcid().equals(emailOrcid)) {
return new ModelAndView("wrong_user");
}
emailEntity.setVerified(true);
emailEntity.setCurrent(true);
emailManager.update(emailEntity);
profileEntityManager.updateLocale(emailOrcid, org.orcid.jaxb.model.common_v2.Locale.fromValue(RequestContextUtils.getLocale(request).toString()));
redirectAttributes.addFlashAttribute("emailVerified", true);
} catch (EncryptionOperationNotPossibleException eonpe) {
LOGGER.warn("Error decypting verify email from the verify email link");
redirectAttributes.addFlashAttribute("invalidVerifyUrl", true);
}
return new ModelAndView("redirect:/my-orcid");
}
private List<OrcidProfile> findPotentialDuplicatesByFirstNameLastName(String firstName, String lastName) {
LOGGER.debug("About to search for potential duplicates during registration for first name={}, last name={}", firstName, lastName);
List<OrcidProfile> orcidProfiles = new ArrayList<OrcidProfile>();
SearchOrcidSolrCriteria queryForm = new SearchOrcidSolrCriteria();
queryForm.setGivenName(firstName);
queryForm.setFamilyName(lastName);
String query = queryForm.deriveQueryString();
OrcidMessage visibleProfiles = orcidSearchManager.findOrcidsByQuery(query, DUP_SEARCH_START, DUP_SEARCH_ROWS);
if (visibleProfiles.getOrcidSearchResults() != null) {
for (OrcidSearchResult searchResult : visibleProfiles.getOrcidSearchResults().getOrcidSearchResult()) {
orcidProfiles.add(searchResult.getOrcidProfile());
}
}
LOGGER.debug("Found {} potential duplicates during registration for first name={}, last name={}", new Object[] { orcidProfiles.size(), firstName, lastName });
return orcidProfiles;
}
private void createMinimalRegistrationAndLogUserIn(HttpServletRequest request, HttpServletResponse response, Registration registration,
boolean usedCaptchaVerification, Locale locale, String ip) {
String unencryptedPassword = registration.getPassword().getValue();
String orcidId = createMinimalRegistration(request, registration, usedCaptchaVerification, locale, ip);
logUserIn(request, response, orcidId, unencryptedPassword);
}
public void logUserIn(HttpServletRequest request, HttpServletResponse response, String orcidId, String password) {
UsernamePasswordAuthenticationToken token = null;
try {
token = new UsernamePasswordAuthenticationToken(orcidId, password);
token.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
if (internalSSOManager.enableCookie()) {
// Set user cookie
internalSSOManager.writeCookie(orcidId, request, response);
}
} catch (AuthenticationException e) {
// this should never happen
SecurityContextHolder.getContext().setAuthentication(null);
LOGGER.warn("User {0} should have been logged-in, but we unable to due to a problem", e, (token != null ? token.getPrincipal() : "empty principle"));
}
}
public String createMinimalRegistration(HttpServletRequest request, Registration registration, boolean usedCaptcha, Locale locale, String ip) {
String sessionId = request.getSession() == null ? null : request.getSession().getId();
String email = registration.getEmail().getValue();
LOGGER.debug("About to create profile from registration email={}, sessionid={}", email, sessionId);
String newUserOrcid = registrationManager.createMinimalRegistration(registration, usedCaptcha, locale, ip);
notificationManager.sendWelcomeEmail(newUserOrcid, email);
request.getSession().setAttribute(EmailConstants.CHECK_EMAIL_VALIDATED, false);
LOGGER.debug("Created profile from registration orcid={}, email={}, sessionid={}",
new Object[] { newUserOrcid, email, sessionId });
return newUserOrcid;
}
}