package edu.harvard.iq.dataverse.confirmemail;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.MailServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider;
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
/**
*
* @author bsilverstein
*/
@Stateless
public class ConfirmEmailServiceBean {
private static final Logger logger = Logger.getLogger(ConfirmEmailServiceBean.class.getCanonicalName());
@EJB
AuthenticationServiceBean dataverseUserService;
@EJB
MailServiceBean mailService;
@EJB
SystemConfig systemConfig;
@PersistenceContext(unitName = "VDCNet-ejbPU")
private EntityManager em;
/**
* Initiate the email confirmation process.
*
* @param user
* @return {@link ConfirmEmailInitResponse}
*/
public ConfirmEmailInitResponse beginConfirm(AuthenticatedUser user) throws ConfirmEmailException {
deleteAllExpiredTokens();
if (user != null) {
return sendConfirm(user, true);
} else {
return new ConfirmEmailInitResponse(false);
}
}
private ConfirmEmailInitResponse sendConfirm(AuthenticatedUser aUser, boolean sendEmail) throws ConfirmEmailException {
// delete old tokens for the user
ConfirmEmailData oldToken = findSingleConfirmEmailDataByUser(aUser);
if (oldToken != null) {
em.remove(oldToken);
}
aUser.setEmailConfirmed(null);
aUser = em.merge(aUser);
// create a fresh token for the user iff they don't have an existing token
ConfirmEmailData confirmEmailData = new ConfirmEmailData(aUser, systemConfig.getMinutesUntilConfirmEmailTokenExpires());
try {
/**
* @todo This "persist" is causing lots of noise in Glassfish's
* server.log if a token already exists (i.e. it isn't expired and
* wasn't deleted above). Exercise this bug by running
* ConfirmEmailIT.
*/
em.persist(confirmEmailData);
ConfirmEmailInitResponse confirmEmailInitResponse = new ConfirmEmailInitResponse(true, confirmEmailData, optionalConfirmEmailAddonMsg(aUser));
if (sendEmail) {
sendLinkOnEmailChange(aUser, confirmEmailInitResponse.getConfirmUrl());
}
return confirmEmailInitResponse;
} catch (Exception ex) {
String msg = "Unable to save token for " + aUser.getEmail();
throw new ConfirmEmailException(msg, ex);
}
}
/**
* @todo: We expect to send two messages. One at signup and another at email
* change.
*/
private void sendLinkOnEmailChange(AuthenticatedUser aUser, String confirmationUrl) throws ConfirmEmailException {
ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil();
String messageBody = BundleUtil.getStringFromBundle("notification.email.changeEmail", Arrays.asList(
aUser.getFirstName(),
confirmationUrl,
confirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires())
));
logger.fine("messageBody:" + messageBody);
try {
String toAddress = aUser.getEmail();
/**
* @todo Move this to Bundle.properties.
*/
String subject = BundleUtil.getStringFromBundle("notification.email.verifyEmail.subject");
mailService.sendSystemEmail(toAddress, subject, messageBody);
} catch (Exception ex) {
/**
* @todo get more specific about the exception that's thrown when
* `asadmin create-javamail-resource` (or equivalent) hasn't been
* run.
*/
throw new ConfirmEmailException("Problem sending email confirmation link possibily due to mail server not being configured.");
}
logger.log(Level.FINE, "attempted to send mail to {0}", aUser.getEmail());
}
/**
* Process the email confirmation token, allowing the user to confirm the
* email address or report on a invalid token.
*
* @param tokenQueried
*/
public ConfirmEmailExecResponse processToken(String tokenQueried) {
deleteAllExpiredTokens();
ConfirmEmailExecResponse tokenUnusable = new ConfirmEmailExecResponse(tokenQueried, null);
ConfirmEmailData confirmEmailData = findSingleConfirmEmailDataByToken(tokenQueried);
if (confirmEmailData != null) {
if (confirmEmailData.isExpired()) {
// shouldn't reach here since tokens are being expired above
return tokenUnusable;
} else {
ConfirmEmailExecResponse goodTokenCanProceed = new ConfirmEmailExecResponse(tokenQueried, confirmEmailData);
if (confirmEmailData == null) {
logger.fine("Invalid token.");
return null;
}
long nowInMilliseconds = new Date().getTime();
Timestamp emailConfirmed = new Timestamp(nowInMilliseconds);
AuthenticatedUser authenticatedUser = confirmEmailData.getAuthenticatedUser();
authenticatedUser.setEmailConfirmed(emailConfirmed);
em.remove(confirmEmailData);
return goodTokenCanProceed;
}
} else {
return tokenUnusable;
}
}
/**
* @param token
* @return Null or a single row of email confirmation data.
*/
private ConfirmEmailData findSingleConfirmEmailDataByToken(String token) {
ConfirmEmailData confirmEmailData = null;
TypedQuery<ConfirmEmailData> typedQuery = em.createNamedQuery("ConfirmEmailData.findByToken", ConfirmEmailData.class);
typedQuery.setParameter("token", token);
try {
confirmEmailData = typedQuery.getSingleResult();
} catch (NoResultException | NonUniqueResultException ex) {
logger.fine("When looking up " + token + " caught " + ex);
}
return confirmEmailData;
}
public ConfirmEmailData findSingleConfirmEmailDataByUser(AuthenticatedUser user) {
ConfirmEmailData confirmEmailData = null;
TypedQuery<ConfirmEmailData> typedQuery = em.createNamedQuery("ConfirmEmailData.findByUser", ConfirmEmailData.class);
typedQuery.setParameter("user", user);
try {
confirmEmailData = typedQuery.getSingleResult();
} catch (NoResultException | NonUniqueResultException ex) {
logger.fine("When looking up user " + user + " caught " + ex);
}
return confirmEmailData;
}
public List<ConfirmEmailData> findAllConfirmEmailData() {
TypedQuery<ConfirmEmailData> typedQuery = em.createNamedQuery("ConfirmEmailData.findAll", ConfirmEmailData.class);
List<ConfirmEmailData> confirmEmailDatas = typedQuery.getResultList();
return confirmEmailDatas;
}
/**
* @return The number of tokens deleted.
*/
private long deleteAllExpiredTokens() {
long numDeleted = 0;
List<ConfirmEmailData> allData = findAllConfirmEmailData();
for (ConfirmEmailData data : allData) {
if (data.isExpired()) {
em.remove(data);
numDeleted++;
}
}
return numDeleted;
}
/**
* @param authenticatedUser
* @return True if token is deleted. False otherwise.
*/
public boolean deleteTokenForUser(AuthenticatedUser authenticatedUser) {
ConfirmEmailData confirmEmailData = findSingleConfirmEmailDataByUser(authenticatedUser);
if (confirmEmailData != null) {
em.remove(confirmEmailData);
return true;
}
return false;
}
public ConfirmEmailData createToken(AuthenticatedUser au) {
ConfirmEmailData confirmEmailData = new ConfirmEmailData(au, systemConfig.getMinutesUntilConfirmEmailTokenExpires());
em.persist(confirmEmailData);
return confirmEmailData;
}
public String optionalConfirmEmailAddonMsg(AuthenticatedUser user) {
ConfirmEmailUtil confirmEmailUtil = new ConfirmEmailUtil();
final String emptyString = "";
if (user == null) {
logger.info("Can't return confirm email message. AuthenticatedUser was null!");
return emptyString;
}
if (ShibAuthenticationProvider.PROVIDER_ID.equals(user.getAuthenticatedUserLookup().getAuthenticationProviderId())) {
// Shib users don't have to confirm their email address.
return emptyString;
}
ConfirmEmailData confirmEmailData = findSingleConfirmEmailDataByUser(user);
if (confirmEmailData == null) {
logger.info("Can't return confirm email message. No ConfirmEmailData for user id " + user.getId());
return emptyString;
}
String expTime = confirmEmailUtil.friendlyExpirationTime(systemConfig.getMinutesUntilConfirmEmailTokenExpires());
String confirmEmailUrl = systemConfig.getDataverseSiteUrl() + "/confirmemail.xhtml?token=" + confirmEmailData.getToken();
List<String> args = Arrays.asList(confirmEmailUrl, expTime);
String optionalConfirmEmailMsg = BundleUtil.getStringFromBundle("notification.email.welcomeConfirmEmailAddOn", args);
return optionalConfirmEmailMsg;
}
}