diff --git a/api/pom.xml b/api/pom.xml index deebfb9e14..0a9d9e2b9b 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -65,6 +65,21 @@ argon2-jvm 2.11 + + net.logicsquad + nanocaptcha + 1.5 + + + org.slf4j + slf4j-api + 1.7.36 + + + com.google.guava + guava + 30.1-jre + org.apache.httpcomponents fluent-hc diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/AbstractCaptchaProvider.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/AbstractCaptchaProvider.java new file mode 100644 index 0000000000..f79e0276a9 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/AbstractCaptchaProvider.java @@ -0,0 +1,47 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.io.IOException; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * The AbstractCaptchaProvider is an abstract class providing a base structure for captcha providers. + * It includes methods for generating refresh challenges, adding captcha-related fields to page context, + * and validating user inputs against captcha challenges. + * + * @see CaptchaBundle + */ +public abstract class AbstractCaptchaProvider { + + protected static final Log log = LogFactory.getLog(AbstractCaptchaProvider.class.getName()); + + /** + * Generates a refresh challenge, typically used for updating the captcha displayed on the page. + * Returns empty CaptchaBundle in case of 3rd party implementations + * + * @return CaptchaBundle containing the refreshed captcha challenge. + * @throws IOException If there is an issue generating the refresh challenge. + */ + abstract CaptchaBundle generateRefreshChallenge() throws IOException; + + /** + * Adds captcha-related fields to the provided page context, allowing integration with web pages. + * + * @param context The context map representing the page's variables. + * @throws IOException If there is an issue adding captcha-related fields to the page context. + */ + abstract void addCaptchaRelatedFieldsToPageContext(Map context) throws IOException; + + /** + * Validates the user input against a captcha challenge identified by the provided challengeId. + * + * @param captchaInput The user's input to be validated. + * @param challengeId The identifier of the captcha challenge (ignored in case of 3rd party implementations). + * @return True if the input is valid, false otherwise. + */ + boolean validateCaptcha(String captchaInput, String challengeId) { + return false; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java new file mode 100644 index 0000000000..5466d5b9a3 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaBundle.java @@ -0,0 +1,57 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.util.Objects; + +/** + * Represents a bundle containing a CAPTCHA image in Base64 format, the associated code, + * and a unique challenge identifier. + * + * @author Ivan Mrsulja + * @version 1.0 + */ +public class CaptchaBundle { + + private final String b64Image; + + private final String code; + + private final String challengeId; + + + public CaptchaBundle(String b64Image, String code, String challengeId) { + this.b64Image = b64Image; + this.code = code; + this.challengeId = challengeId; + } + + public String getB64Image() { + return b64Image; + } + + public String getCode() { + return code; + } + + public String getCaptchaId() { + return challengeId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CaptchaBundle that = (CaptchaBundle) o; + return Objects.equals(code, that.code) && Objects.equals(challengeId, that.challengeId); + } + + @Override + public int hashCode() { + return Objects.hash(code, challengeId); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaDifficulty.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaDifficulty.java new file mode 100644 index 0000000000..5b2bd17f09 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaDifficulty.java @@ -0,0 +1,6 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +public enum CaptchaDifficulty { + EASY, + HARD +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaImplementation.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaImplementation.java new file mode 100644 index 0000000000..7f55b43d8f --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaImplementation.java @@ -0,0 +1,7 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +public enum CaptchaImplementation { + RECAPTCHAV2, + NANOCAPTCHA, + NONE; +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java new file mode 100644 index 0000000000..e5a92b039b --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/CaptchaServiceBean.java @@ -0,0 +1,138 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Strings; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; + + +/** + * This class provides services related to CAPTCHA challenges and validation. + * It includes method delegates for generating challenges, validating challenge responses, + * and managing CAPTCHA challenges for specific hosts. + * + * @author Ivan Mrsulja + * @version 1.0 + */ +public class CaptchaServiceBean { + + private static final Cache captchaChallenges = + CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(); + + private static AbstractCaptchaProvider captchaProvider; + + static { + CaptchaImplementation captchaImplementation = getCaptchaImpl(); + switch (captchaImplementation) { + case RECAPTCHAV2: + captchaProvider = new Recaptchav2Provider(); + break; + case NANOCAPTCHA: + captchaProvider = new NanocaptchaProvider(); + break; + case NONE: + captchaProvider = new DummyCaptchaProvider(); + break; + } + } + + /** + * Generates a new CAPTCHA challenge (returns empty CaptchaBundle for 3rd party providers). + * + * @return A CaptchaBundle containing the CAPTCHA image in Base64 format, the content, + * and a unique identifier. + * @throws IOException If an error occurs during image conversion. + */ + public static CaptchaBundle generateRefreshedChallenge() throws IOException { + return captchaProvider.generateRefreshChallenge(); + } + + /** + * Retrieves a CAPTCHA challenge for a specific host based on the provided CAPTCHA ID + * Removes the challenge from the storage after retrieval. + * + * @param captchaId The CAPTCHA ID to match. + * @return An Optional containing the CaptchaBundle if a matching challenge is found, + * or an empty Optional otherwise. + */ + public static Optional getChallenge(String captchaId) { + CaptchaBundle challengeForHost = captchaChallenges.getIfPresent(captchaId); + if (challengeForHost == null) { + return Optional.empty(); + } + + captchaChallenges.invalidate(captchaId); + + return Optional.of(challengeForHost); + } + + /** + * Gets the map containing CAPTCHA challenges for different hosts. + * + * @return A ConcurrentHashMap with host addresses as keys and CaptchaBundle objects as values. + */ + public static Cache getCaptchaChallenges() { + return captchaChallenges; + } + + /** + * Retrieves the configured captcha implementation based on the application's configuration properties. + * If captcha functionality is disabled, returns NONE. If the captcha implementation is not specified, + * defaults to NANOCAPTCHA. + * + * @return The selected captcha implementation (NANOCAPTCHA, RECAPTCHAv2, or NONE). + */ + public static CaptchaImplementation getCaptchaImpl() { + String captchaEnabledSetting = ConfigurationProperties.getInstance().getProperty("captcha.enabled"); + + if (Objects.nonNull(captchaEnabledSetting) && !Boolean.parseBoolean(captchaEnabledSetting)) { + return CaptchaImplementation.NONE; + } + + String captchaImplSetting = + ConfigurationProperties.getInstance().getProperty("captcha.implementation"); + + if (Strings.isNullOrEmpty(captchaImplSetting) || + (!captchaImplSetting.equalsIgnoreCase(CaptchaImplementation.RECAPTCHAV2.name()) && + !captchaImplSetting.equalsIgnoreCase(CaptchaImplementation.NANOCAPTCHA.name()))) { + captchaImplSetting = CaptchaImplementation.NANOCAPTCHA.name(); + } + + return CaptchaImplementation.valueOf(captchaImplSetting.toUpperCase()); + } + + /** + * Adds captcha-related fields to the given page context map. The specific fields added depend on the + * configured captcha implementation. + * + * @param context The page context map to which captcha-related fields are added. + * @throws IOException If there is an IO error during captcha challenge generation. + */ + public static void addCaptchaRelatedFieldsToPageContext(Map context) throws IOException { + CaptchaImplementation captchaImpl = getCaptchaImpl(); + context.put("captchaToUse", captchaImpl.name()); + captchaProvider.addCaptchaRelatedFieldsToPageContext(context); + } + + /** + * Validates a user's captcha input. + * + * @param captchaInput The user's input for the captcha challenge. + * @param challengeId The unique identifier for the challenge (if captcha is 3rd party, this param is ignored). + * @return {@code true} if the captcha input is valid, {@code false} otherwise. + */ + public static boolean validateCaptcha(String captchaInput, String challengeId) { + return captchaProvider.validateCaptcha(captchaInput, challengeId); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/DummyCaptchaProvider.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/DummyCaptchaProvider.java new file mode 100644 index 0000000000..923f6fcb13 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/DummyCaptchaProvider.java @@ -0,0 +1,29 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.util.Map; + +/** + * DummyCaptchaProvider is a concrete implementation of AbstractCaptchaProvider, + * serving as a fallback when CAPTCHA is disabled. Validation will always pass, + * in order to fulfill validation logic. + * + * @see AbstractCaptchaProvider + * @see CaptchaBundle + */ +public class DummyCaptchaProvider extends AbstractCaptchaProvider { + + @Override + CaptchaBundle generateRefreshChallenge() { + return new CaptchaBundle("", "", ""); // No refresh challenges if there is no implementation + } + + @Override + void addCaptchaRelatedFieldsToPageContext(Map context) { + // No added fields necessary if there is no implementation + } + + @Override + boolean validateCaptcha(String captchaInput, String challengeId) { + return true; // validation always passes + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/NanocaptchaProvider.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/NanocaptchaProvider.java new file mode 100644 index 0000000000..630a8bcf27 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/NanocaptchaProvider.java @@ -0,0 +1,126 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.imageio.ImageIO; + +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import net.logicsquad.nanocaptcha.image.ImageCaptcha; +import net.logicsquad.nanocaptcha.image.backgrounds.GradiatedBackgroundProducer; +import net.logicsquad.nanocaptcha.image.filter.FishEyeImageFilter; +import net.logicsquad.nanocaptcha.image.filter.StretchImageFilter; +import net.logicsquad.nanocaptcha.image.noise.CurvedLineNoiseProducer; +import net.logicsquad.nanocaptcha.image.noise.StraightLineNoiseProducer; + +/** + * NanocaptchaProvider generates and manages captcha challenges using Nanocaptcha. + * This class extends AbstractCaptchaProvider and supports easy and hard difficulty levels. + * + * @see AbstractCaptchaProvider + * @see CaptchaBundle + * @see CaptchaServiceBean + * @see CaptchaDifficulty + */ +public class NanocaptchaProvider extends AbstractCaptchaProvider { + + private final SecureRandom random = new SecureRandom(); + + @Override + public CaptchaBundle generateRefreshChallenge() throws IOException { + return generateChallenge(); + } + + @Override + public void addCaptchaRelatedFieldsToPageContext(Map context) throws IOException { + CaptchaBundle captchaChallenge = generateChallenge(); + CaptchaServiceBean.getCaptchaChallenges().put(captchaChallenge.getCaptchaId(), captchaChallenge); + + context.put("challenge", captchaChallenge.getB64Image()); + context.put("challengeId", captchaChallenge.getCaptchaId()); + } + + @Override + public boolean validateCaptcha(String captchaInput, String challengeId) { + Optional optionalChallenge = CaptchaServiceBean.getChallenge(challengeId); + return optionalChallenge.isPresent() && optionalChallenge.get().getCode().equals(captchaInput); + } + + /** + * Generates a captcha challenge. + * + * @return CaptchaBundle containing the captcha image encoded in Base64, + * captcha content, and a randomly generated UUID. + * @throws IOException If there is an issue generating the captcha. + */ + private CaptchaBundle generateChallenge() throws IOException { + CaptchaDifficulty difficulty = getCaptchaDifficulty(); + ImageCaptcha.Builder imageCaptchaBuilder = new ImageCaptcha.Builder(220, 85) + .addContent(random.nextInt(2) + 5) + .addBackground(new GradiatedBackgroundProducer()) + .addNoise(new StraightLineNoiseProducer(getRandomColor(), 2)) + .addFilter(new StretchImageFilter()) + .addBorder(); + + if (difficulty.equals(CaptchaDifficulty.HARD)) { + imageCaptchaBuilder + .addNoise(new CurvedLineNoiseProducer(getRandomColor(), 2f)) + .addFilter(new StretchImageFilter()) + .addFilter(new FishEyeImageFilter()) + .build(); + } + + ImageCaptcha imageCaptcha = imageCaptchaBuilder.build(); + return new CaptchaBundle(convertToBase64(imageCaptcha.getImage()), imageCaptcha.getContent(), + UUID.randomUUID().toString()); + } + + /** + * Converts a BufferedImage object to Base64 format. + * + * @param image The BufferedImage to convert. + * @return The Base64-encoded string representation of the image. + * @throws IOException If an error occurs during image conversion. + */ + private String convertToBase64(BufferedImage image) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "png", baos); + byte[] imageBytes = baos.toByteArray(); + + return Base64.getEncoder().encodeToString(imageBytes); + } + + /** + * Retrieves the configured difficulty level for generating captchas. + * If the difficulty level is not specified or is not HARD, the default difficulty is set to EASY. + * + * @return The difficulty level for captcha generation (EASY or HARD). + */ + private CaptchaDifficulty getCaptchaDifficulty() { + String difficulty = ConfigurationProperties.getInstance().getProperty("nanocaptcha.difficulty"); + try { + return CaptchaDifficulty.valueOf(difficulty.toUpperCase()); + } catch (NullPointerException | IllegalArgumentException e) { + return CaptchaDifficulty.EASY; + } + } + + /** + * Generates a random Color object. + * + * @return A randomly generated Color object. + */ + private Color getRandomColor() { + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + return new Color(r, g, b); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java new file mode 100644 index 0000000000..ae1420e383 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/ReCaptchaResponse.java @@ -0,0 +1,64 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.util.Date; + +/** + * Represents the response from Google's reCAPTCHA API. + * It includes information about the success of the reCAPTCHA verification, + * the timestamp of the challenge, and the hostname associated with the verification. + * + * @author Ivan Mrsulja + * @version 1.0 + */ +public class ReCaptchaResponse { + + private boolean success; + + private Date challenge_ts; + + private String hostname; + + public ReCaptchaResponse() { + } + + public ReCaptchaResponse(boolean success, Date challenge_ts, String hostname) { + this.success = success; + this.challenge_ts = challenge_ts; + this.hostname = hostname; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public Date getChallenge_ts() { + return challenge_ts; + } + + public void setChallenge_ts(Date challenge_ts) { + this.challenge_ts = challenge_ts; + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + @Override + public String toString() { + return "ReCaptchaResponse{" + + "success=" + success + + ", challenge_ts=" + challenge_ts + + ", hostname='" + hostname + '\'' + + '}'; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/Recaptchav2Provider.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/Recaptchav2Provider.java new file mode 100644 index 0000000000..163746885f --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/Recaptchav2Provider.java @@ -0,0 +1,69 @@ +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +/** + * Recaptchav2Provider generates and manages captcha challenges using Google RECAPTCHAv2. + * This class extends AbstractCaptchaProvider. + * + * @see AbstractCaptchaProvider + * @see CaptchaBundle + */ +public class Recaptchav2Provider extends AbstractCaptchaProvider { + + @Override + public CaptchaBundle generateRefreshChallenge() { + return new CaptchaBundle("", "", ""); // RECAPTCHAv2 does not generate refresh challenges on backend side + } + + @Override + public void addCaptchaRelatedFieldsToPageContext(Map context) { + context.put("siteKey", + Objects.requireNonNull(ConfigurationProperties.getInstance().getProperty("recaptcha.siteKey"), + "You have to provide a site key through configuration file.")); + } + + @Override + public boolean validateCaptcha(String captchaInput, String challengeId) { + return validateReCaptcha(captchaInput); + } + + /** + * Validates a reCAPTCHA response using Google's reCAPTCHA API. + * + * @param recaptchaResponse The reCAPTCHA response to validate. + * @return True if the reCAPTCHA response is valid, false otherwise. + */ + public boolean validateReCaptcha(String recaptchaResponse) { + String secretKey = + Objects.requireNonNull(ConfigurationProperties.getInstance().getProperty("recaptcha.secretKey"), + "You have to provide a secret key through configuration file."); + String verificationUrl = + "https://www.google.com/recaptcha/api/siteverify?secret=" + secretKey + "&response=" + recaptchaResponse; + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet verificationRequest = new HttpGet(verificationUrl); + HttpResponse verificationResponse = httpClient.execute(verificationRequest); + + String responseBody = EntityUtils.toString(verificationResponse.getEntity()); + ObjectMapper objectMapper = new ObjectMapper(); + ReCaptchaResponse response = objectMapper.readValue(responseBody, ReCaptchaResponse.class); + + return response.isSuccess(); + } catch (IOException e) { + log.warn("ReCaptcha validation failed."); + } + + return false; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java new file mode 100644 index 0000000000..3e4c7a3e38 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/beans/RefreshCaptchaController.java @@ -0,0 +1,33 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.beans; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.twelvemonkeys.servlet.HttpServlet; + +@WebServlet(name = "refreshCaptcha", urlPatterns = {"/refreshCaptcha"}, loadOnStartup = 5) +public class RefreshCaptchaController extends HttpServlet { + + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String oldChallengeId = request.getParameter("oldChallengeId"); + + response.setContentType("application/json"); + PrintWriter out = response.getWriter(); + + CaptchaBundle newChallenge = CaptchaServiceBean.generateRefreshedChallenge(); + CaptchaServiceBean.getCaptchaChallenges().invalidate(oldChallengeId); + CaptchaServiceBean.getCaptchaChallenges().put(newChallenge.getCaptchaId(), newChallenge); + + out.println("{\"challenge\": \"" + newChallenge.getB64Image() + "\", \"challengeId\": \"" + + newChallenge.getCaptchaId() + "\"}"); + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java index 9c33959562..00838f1be2 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactFormController.java @@ -2,20 +2,24 @@ package edu.cornell.mannlib.vitro.webapp.controller.freemarker; +import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Objects; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import javax.servlet.annotation.WebServlet; import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailFactory; - -import javax.servlet.annotation.WebServlet; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * Controller for comments ("contact us") page @@ -30,14 +34,14 @@ public class ContactFormController extends FreemarkerHttpServlet { private static final String TEMPLATE_DEFAULT = "contactForm-form.ftl"; private static final String TEMPLATE_ERROR = "contactForm-error.ftl"; + @Override protected String getTitle(String siteName, VitroRequest vreq) { return siteName + " Feedback Form"; } @Override - protected ResponseValues processRequest(VitroRequest vreq) { - + protected ResponseValues processRequest(VitroRequest vreq) throws IOException { ApplicationBean appBean = vreq.getAppBean(); String templateName; @@ -58,7 +62,9 @@ else if (StringUtils.isBlank(appBean.getContactMail())) { } else { + CaptchaServiceBean.addCaptchaRelatedFieldsToPageContext(body); + body.put("contextPath", vreq.getContextPath()); body.put("formAction", "submitFeedback"); if (vreq.getHeader("Referer") == null) { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java index 1444e9115b..9361d51974 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/ContactMailController.java @@ -11,6 +11,8 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import javax.mail.Address; import javax.mail.Message; @@ -24,28 +26,31 @@ import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; import edu.cornell.mannlib.vitro.webapp.beans.ApplicationBean; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaImplementation; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.TemplateProcessingHelper.TemplateProcessingException; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; import edu.cornell.mannlib.vitro.webapp.email.FreemarkerEmailFactory; +import edu.cornell.mannlib.vitro.webapp.i18n.I18n; +import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; @WebServlet(name = "sendMail", urlPatterns = {"/submitFeedback"}, loadOnStartup = 5) public class ContactMailController extends FreemarkerHttpServlet { - private static final Log log = LogFactory - .getLog(ContactMailController.class); + private static final Log log = LogFactory + .getLog(ContactMailController.class); private static final long serialVersionUID = 1L; - private final static String SPAM_MESSAGE = "Your message was flagged as spam."; + private final static String SPAM_MESSAGE = "Your message was flagged as spam."; - private final static String WEB_USERNAME_PARAM = "webusername"; + private final static String WEB_USERNAME_PARAM = "webusername"; private final static String WEB_USEREMAIL_PARAM = "webuseremail"; - private final static String COMMENTS_PARAM = "s34gfd88p9x1"; + private final static String COMMENTS_PARAM = "s34gfd88p9x1"; private final static String TEMPLATE_CONFIRMATION = "contactForm-confirmation.ftl"; private final static String TEMPLATE_EMAIL = "contactForm-email.ftl"; @@ -53,135 +58,148 @@ public class ContactMailController extends FreemarkerHttpServlet { private final static String TEMPLATE_ERROR = "contactForm-error.ftl"; private final static String TEMPLATE_FORM = "contactForm-form.ftl"; - private static final String EMAIL_JOURNAL_FILE_DIR = "emailJournal"; - private static final String EMAIL_JOURNAL_FILE_NAME = "contactFormEmails.html"; + private static final String EMAIL_JOURNAL_FILE_DIR = "emailJournal"; + private static final String EMAIL_JOURNAL_FILE_NAME = "contactFormEmails.html"; + + private CaptchaImplementation captchaImpl; - @Override + @Override protected String getTitle(String siteName, VitroRequest vreq) { return siteName + " Feedback Form"; } @Override - protected ResponseValues processRequest(VitroRequest vreq) { - if (!FreemarkerEmailFactory.isConfigured(vreq)) { - return errorNoSmtpServer(); - } - - String[] recipients = figureRecipients(vreq); - if (recipients.length == 0) { - return errorNoRecipients(); - } - - String webusername = nonNullAndTrim(vreq, WEB_USERNAME_PARAM); - String webuseremail = nonNullAndTrim(vreq, WEB_USEREMAIL_PARAM); - String comments = nonNullAndTrim(vreq, COMMENTS_PARAM); - String formType = nonNullAndTrim(vreq, "DeliveryType"); - String captchaInput = nonNullAndTrim(vreq, "defaultReal"); - String captchaDisplay = nonNullAndTrim(vreq, "defaultRealHash"); - - String errorMsg = validateInput(webusername, webuseremail, comments, captchaInput, captchaDisplay); - - if ( errorMsg != null) { - return errorParametersNotValid(errorMsg, webusername, webuseremail, comments); - } - - String spamReason = checkForSpam(comments, formType); - if (spamReason != null) { - return errorSpam(); - } - - return processValidRequest(vreq, webusername, webuseremail, recipients, comments); - } - - private String[] figureRecipients(VitroRequest vreq) { - String contactMailAddresses = vreq.getAppBean().getContactMail().trim(); - if ((contactMailAddresses == null) || contactMailAddresses.isEmpty()) { - return new String[0]; - } - - return contactMailAddresses.split(","); - } - - private ResponseValues processValidRequest(VitroRequest vreq, - String webusername, String webuseremail, String[] recipients, - String comments) throws Error { - String statusMsg = null; // holds the error status - - ApplicationBean appBean = vreq.getAppBean(); - String deliveryfrom = "Message from the " + appBean.getApplicationName() + " Contact Form"; - - String originalReferer = getOriginalRefererFromSession(vreq); - - String msgText = composeEmail(webusername, webuseremail, comments, - deliveryfrom, originalReferer, vreq.getRemoteAddr(), vreq); - - try { - // Write the message to the journal file - FileWriter fw = new FileWriter(locateTheJournalFile(), true); - PrintWriter outFile = new PrintWriter(fw); - writeBackupCopy(outFile, msgText, vreq); - - try { - // Send the message - Session s = FreemarkerEmailFactory.getEmailSession(vreq); - sendMessage(s, webuseremail, webusername, recipients, deliveryfrom, msgText); - } catch (AddressException e) { - statusMsg = "Please supply a valid email address."; - outFile.println( statusMsg ); - outFile.println( e.getMessage() ); - } catch (SendFailedException e) { - statusMsg = "The system was unable to deliver your mail. Please try again later. [SEND FAILED]"; - outFile.println( statusMsg ); - outFile.println( e.getMessage() ); - } catch (MessagingException e) { - statusMsg = "The system was unable to deliver your mail. Please try again later. [MESSAGING]"; - outFile.println( statusMsg ); - outFile.println( e.getMessage() ); - e.printStackTrace(); - } - - outFile.close(); - } - catch (IOException e){ - log.error("Can't open file to write email backup"); - } - - if (statusMsg == null) { - // Message was sent successfully - return new TemplateResponseValues(TEMPLATE_CONFIRMATION); - } else { - Map body = new HashMap(); - body.put("errorMessage", statusMsg); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } - } - - /** - * The journal file belongs in a sub-directory of the Vitro home directory. - * If the sub-directory doesn't exist, create it. - */ - private File locateTheJournalFile() { - File homeDir = ApplicationUtils.instance().getHomeDirectory().getPath().toFile(); - File journalDir = new File(homeDir, EMAIL_JOURNAL_FILE_DIR); - if (!journalDir.exists()) { - boolean created = journalDir.mkdir(); - if (!created) { - throw new IllegalStateException( - "Unable to create email journal directory at '" - + journalDir + "'"); - } - } - - File journalFile = new File(journalDir, EMAIL_JOURNAL_FILE_NAME); - return journalFile; - } - - - private String getOriginalRefererFromSession(VitroRequest vreq) { - String originalReferer = (String) vreq.getSession().getAttribute("contactFormReferer"); - if (originalReferer != null) { - vreq.getSession().removeAttribute("contactFormReferer"); + protected ResponseValues processRequest(VitroRequest vreq) throws IOException { + if (!FreemarkerEmailFactory.isConfigured(vreq)) { + return errorNoSmtpServer(); + } + + String[] recipients = figureRecipients(vreq); + if (recipients.length == 0) { + return errorNoRecipients(); + } + + captchaImpl = CaptchaServiceBean.getCaptchaImpl(); + + String webusername = nonNullAndTrim(vreq, WEB_USERNAME_PARAM); + String webuseremail = nonNullAndTrim(vreq, WEB_USEREMAIL_PARAM); + String comments = nonNullAndTrim(vreq, COMMENTS_PARAM); + String formType = nonNullAndTrim(vreq, "DeliveryType"); + + String captchaInput; + String captchaId = ""; + switch (captchaImpl) { + case RECAPTCHAV2: + captchaInput = nonNullAndTrim(vreq, "g-recaptcha-response"); + break; + case NANOCAPTCHA: + default: + captchaInput = nonNullAndTrim(vreq, "userSolution"); + captchaId = nonNullAndTrim(vreq, "challengeId"); + } + + String errorMsg = validateInput(webusername, webuseremail, comments, captchaInput, captchaId, vreq); + + if (errorMsg != null) { + return errorParametersNotValid(errorMsg, webusername, webuseremail, comments, vreq.getContextPath()); + } + + String spamReason = checkForSpam(comments, formType); + if (spamReason != null) { + return errorSpam(); + } + + return processValidRequest(vreq, webusername, webuseremail, recipients, comments); + } + + private String[] figureRecipients(VitroRequest vreq) { + String contactMailAddresses = vreq.getAppBean().getContactMail().trim(); + if ((contactMailAddresses == null) || contactMailAddresses.isEmpty()) { + return new String[0]; + } + + return contactMailAddresses.split(","); + } + + private ResponseValues processValidRequest(VitroRequest vreq, + String webusername, String webuseremail, String[] recipients, + String comments) throws Error { + String statusMsg = null; // holds the error status + + ApplicationBean appBean = vreq.getAppBean(); + String deliveryfrom = "Message from the " + appBean.getApplicationName() + " Contact Form"; + + String originalReferer = getOriginalRefererFromSession(vreq); + + String msgText = composeEmail(webusername, webuseremail, comments, + deliveryfrom, originalReferer, vreq.getRemoteAddr(), vreq); + + try { + // Write the message to the journal file + FileWriter fw = new FileWriter(locateTheJournalFile(), true); + PrintWriter outFile = new PrintWriter(fw); + writeBackupCopy(outFile, msgText, vreq); + + try { + // Send the message + Session s = FreemarkerEmailFactory.getEmailSession(vreq); + sendMessage(s, webuseremail, webusername, recipients, deliveryfrom, msgText); + } catch (AddressException e) { + statusMsg = "Please supply a valid email address."; + outFile.println(statusMsg); + outFile.println(e.getMessage()); + } catch (SendFailedException e) { + statusMsg = "The system was unable to deliver your mail. Please try again later. [SEND FAILED]"; + outFile.println(statusMsg); + outFile.println(e.getMessage()); + } catch (MessagingException e) { + statusMsg = "The system was unable to deliver your mail. Please try again later. [MESSAGING]"; + outFile.println(statusMsg); + outFile.println(e.getMessage()); + e.printStackTrace(); + } + + outFile.close(); + } catch (IOException e) { + log.error("Can't open file to write email backup"); + } + + if (statusMsg == null) { + // Message was sent successfully + return new TemplateResponseValues(TEMPLATE_CONFIRMATION); + } else { + Map body = new HashMap(); + body.put("errorMessage", statusMsg); + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + } + + /** + * The journal file belongs in a sub-directory of the Vitro home directory. + * If the sub-directory doesn't exist, create it. + */ + private File locateTheJournalFile() { + File homeDir = ApplicationUtils.instance().getHomeDirectory().getPath().toFile(); + File journalDir = new File(homeDir, EMAIL_JOURNAL_FILE_DIR); + if (!journalDir.exists()) { + boolean created = journalDir.mkdir(); + if (!created) { + throw new IllegalStateException( + "Unable to create email journal directory at '" + + journalDir + "'"); + } + } + + File journalFile = new File(journalDir, EMAIL_JOURNAL_FILE_NAME); + return journalFile; + } + + + private String getOriginalRefererFromSession(VitroRequest vreq) { + String originalReferer = (String) vreq.getSession().getAttribute("contactFormReferer"); + if (originalReferer != null) { + vreq.getSession().removeAttribute("contactFormReferer"); /* does not support legitimate clients that don't send the Referer header String referer = request.getHeader("Referer"); if (referer == null || @@ -192,25 +210,28 @@ private String getOriginalRefererFromSession(VitroRequest vreq) { statusMsg = SPAM_MESSAGE; } */ - } else { - originalReferer = "none"; - } - return originalReferer; - } - - /** Intended to mangle url so it can get through spam filtering - * http://host/dir/servlet?param=value -> host: dir/servlet?param=value */ - public String stripProtocol( String in ){ - if( in == null ) + } else { + originalReferer = "none"; + } + return originalReferer; + } + + /** + * Intended to mangle url so it can get through spam filtering + * http://host/dir/servlet?param=value -> host: dir/servlet?param=value + */ + public String stripProtocol(String in) { + if (in == null) { return ""; - else - return in.replaceAll("http://", "host: " ); + } else { + return in.replaceAll("http://", "host: "); + } } private String composeEmail(String webusername, String webuseremail, - String comments, String deliveryfrom, - String originalReferer, String ipAddr, - HttpServletRequest request) { + String comments, String deliveryfrom, + String originalReferer, String ipAddr, + HttpServletRequest request) { Map email = new HashMap(); String template = TEMPLATE_EMAIL; @@ -220,7 +241,7 @@ private String composeEmail(String webusername, String webuseremail, email.put("emailAddress", webuseremail); email.put("comments", comments); email.put("ip", ipAddr); - if ( !(originalReferer == null || originalReferer.equals("none")) ) { + if (!(originalReferer == null || originalReferer.equals("none"))) { email.put("referrer", UrlBuilder.urlDecode(originalReferer)); } @@ -233,13 +254,13 @@ private String composeEmail(String webusername, String webuseremail, } private void writeBackupCopy(PrintWriter outFile, String msgText, - HttpServletRequest request) { + HttpServletRequest request) { Map backup = new HashMap(); String template = TEMPLATE_BACKUP; - Calendar cal = Calendar.getInstance(); - backup.put("datetime", cal.getTime().toString()); + Calendar cal = Calendar.getInstance(); + backup.put("datetime", cal.getTime().toString()); backup.put("msgText", msgText); try { @@ -253,77 +274,78 @@ private void writeBackupCopy(PrintWriter outFile, String msgText, } private void sendMessage(Session s, String webuseremail, String webusername, - String[] recipients, String deliveryfrom, String msgText) - throws AddressException, SendFailedException, MessagingException { + String[] recipients, String deliveryfrom, String msgText) + throws MessagingException { // Construct the message - MimeMessage msg = new MimeMessage( s ); + MimeMessage msg = new MimeMessage(s); //System.out.println("trying to send message from servlet"); // Set the reply address try { - msg.setReplyTo( new Address[] { new InternetAddress( webuseremail, webusername ) } ); + msg.setReplyTo(new Address[] {new InternetAddress(webuseremail, webusername)}); } catch (UnsupportedEncodingException e) { - log.error("Can't set message reply with personal name " + webusername + - " due to UnsupportedEncodingException"); + log.error("Can't set message reply with personal name " + webusername + + " due to UnsupportedEncodingException"); // msg.setFrom( new InternetAddress( webuseremail ) ); } // Set the recipient address - InternetAddress[] address=new InternetAddress[recipients.length]; - for (int i=0; i 0) { - msg.setFrom(address[0]); - } else { - msg.setFrom( new InternetAddress( webuseremail ) ); - } + // Set the from address + if (address != null && address.length > 0) { + msg.setFrom(address[0]); + } else { + msg.setFrom(new InternetAddress(webuseremail)); + } // Set the subject and text - msg.setSubject( deliveryfrom ); + msg.setSubject(deliveryfrom); // add the multipart to the message - msg.setContent(msgText,"text/html; charset=UTF-8"); + msg.setContent(msgText, "text/html; charset=UTF-8"); // set the Date: header - msg.setSentDate( new Date() ); + msg.setSentDate(new Date()); - Transport.send( msg ); // try to send the message via smtp - catch error exceptions + Transport.send(msg); // try to send the message via smtp - catch error exceptions } - private String nonNullAndTrim(HttpServletRequest req, String key) { - String value = req.getParameter(key); - return (value == null) ? "" : value.trim(); - } + private String nonNullAndTrim(HttpServletRequest req, String key) { + String value = req.getParameter(key); + return (value == null) ? "" : value.trim(); + } private String validateInput(String webusername, String webuseremail, - String comments, String captchaInput, String captchaDisplay) { + String comments, String captchaInput, String challengeId, VitroRequest vreq) { + I18nBundle i18nBundle = I18n.bundle(vreq); - if( webusername.isEmpty() ){ - return "Please enter a value in the Full name field."; + if (webusername.isEmpty()) { + return i18nBundle.text("full_name_empty"); } - if( webuseremail.isEmpty() ){ - return "Please enter a valid email address."; + if (webuseremail.isEmpty()) { + return i18nBundle.text("email_address_empty"); } if (comments.isEmpty()) { - return "Please enter your comments or questions in the space provided."; + return i18nBundle.text("comments_empty"); } - if (captchaInput.isEmpty()) { - return "Please enter the contents of the gray box in the security field provided."; + if (!captchaImpl.equals(CaptchaImplementation.NONE) && captchaInput.isEmpty()) { + return i18nBundle.text("captcha_user_sol_empty"); } - if ( !captchaHash(captchaInput).equals(captchaDisplay) ) { - return "The value you entered in the security field did not match the letters displayed in the gray box."; - } + if (CaptchaServiceBean.validateCaptcha(captchaInput, challengeId)) { + return null; + } - return null; + return i18nBundle.text("captcha_user_sol_invalid"); } /** @@ -331,22 +353,22 @@ private String validateInput(String webusername, String webuseremail, * containing the reason the message was flagged as spam. */ private String checkForSpam(String comments, String formType) { - /* If the form doesn't specify a delivery type, treat as spam. */ - if (!"contact".equals(formType)) { - return "The form specifies no delivery type."; - } + /* If the form doesn't specify a delivery type, treat as spam. */ + if (!"contact".equals(formType)) { + return "The form specifies no delivery type."; + } /* if this blog markup is found, treat comment as blog spam */ if ( (comments.indexOf("[/url]") > -1 - || comments.indexOf("[/URL]") > -1 - || comments.indexOf("[url=") > -1 - || comments.indexOf("[URL=") > -1)) { + || comments.indexOf("[/URL]") > -1 + || comments.indexOf("[url=") > -1 + || comments.indexOf("[URL=") > -1)) { return "The message contained blog link markup."; } /* if message is absurdly short, treat as blog spam */ - if (comments.length()<15) { + if (comments.length() < 15) { return "The message was too short."; } @@ -354,45 +376,51 @@ private String checkForSpam(String comments, String formType) { } - private String captchaHash(String value) { - int hash = 5381; - value = value.toUpperCase(); - for(int i = 0; i < value.length(); i++) { - hash = ((hash << 5) + hash) + value.charAt(i); - } - return String.valueOf(hash); - } + private String captchaHash(String value) { + int hash = 5381; + value = value.toUpperCase(); + for (int i = 0; i < value.length(); i++) { + hash = ((hash << 5) + hash) + value.charAt(i); + } + return String.valueOf(hash); + } - private ResponseValues errorNoSmtpServer() { + private ResponseValues errorNoSmtpServer() { Map body = new HashMap(); body.put("errorMessage", - "This application has not yet been configured to send mail. " + + "This application has not yet been configured to send mail. " + "Email properties must be specified in the configuration properties file."); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } - - private ResponseValues errorNoRecipients() { - Map body = new HashMap(); - body.put("errorMessage", "To establish the Contact Us mail capability " - + "the system administrators must specify " - + "at least one email address."); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } - - private ResponseValues errorParametersNotValid(String errorMsg, String webusername, String webuseremail, String comments) { + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + + private ResponseValues errorNoRecipients() { Map body = new HashMap(); - body.put("errorMessage", errorMsg); - body.put("formAction", "submitFeedback"); - body.put("webusername", webusername); - body.put("webuseremail", webuseremail); - body.put("comments", comments); - return new TemplateResponseValues(TEMPLATE_FORM, body); - } - - private ResponseValues errorSpam() { - Map body = new HashMap(); - body.put("errorMessage", SPAM_MESSAGE); - return new TemplateResponseValues(TEMPLATE_ERROR, body); - } + body.put("errorMessage", "To establish the Contact Us mail capability " + + "the system administrators must specify " + + "at least one email address."); + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } + private ResponseValues errorParametersNotValid(String errorMsg, String webusername, String webuseremail, + String comments, String contextPath) + throws IOException { + Map body = new HashMap<>(); + body.put("errorMessage", errorMsg); + body.put("formAction", "submitFeedback"); + body.put("webusername", webusername); + body.put("webuseremail", webuseremail); + body.put("comments", comments); + body.put("captchaToUse", captchaImpl); + body.put("contextPath", contextPath); + + CaptchaServiceBean.addCaptchaRelatedFieldsToPageContext(body); + + return new TemplateResponseValues(TEMPLATE_FORM, body); + } + + private ResponseValues errorSpam() { + Map body = new HashMap(); + body.put("errorMessage", SPAM_MESSAGE); + return new TemplateResponseValues(TEMPLATE_ERROR, body); + } } diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java new file mode 100644 index 0000000000..4d9b4d169a --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/CaptchaServiceBeanTest.java @@ -0,0 +1,142 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaImplementation; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import org.junit.Before; +import org.junit.Test; +import stubs.edu.cornell.mannlib.vitro.webapp.config.ConfigurationPropertiesStub; +import stubs.javax.servlet.ServletContextStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import stubs.javax.servlet.http.HttpSessionStub; + +public class CaptchaServiceBeanTest extends AbstractTestClass { + + private final ConfigurationPropertiesStub props = new ConfigurationPropertiesStub(); + + @Before + public void createConfigurationProperties() { + props.setProperty("captcha.enabled", "true"); + ServletContextStub ctx = new ServletContextStub(); + ConfigurationProperties.setInstance(props); + + HttpSessionStub session = new HttpSessionStub(); + session.setServletContext(ctx); + + HttpServletRequestStub httpServletRequest = new HttpServletRequestStub(); + httpServletRequest.setSession(session); + } + + @Test + public void getChallenge_MatchingCaptchaIdAndRemoteAddress_ReturnsCaptchaBundle() { + // Given + String captchaId = "sampleCaptchaId"; + CaptchaBundle sampleChallenge = new CaptchaBundle("sampleB64Image", "sampleCode", captchaId); + CaptchaServiceBean.getCaptchaChallenges().put(captchaId, sampleChallenge); + + // When + Optional result = CaptchaServiceBean.getChallenge(captchaId); + + // Then + assertTrue(result.isPresent()); + assertEquals(sampleChallenge, result.get()); + } + + @Test + public void getChallenge_NonMatchingCaptchaIdAndRemoteAddress_ReturnsEmptyOptional() { + // When + Optional result = CaptchaServiceBean.getChallenge("nonMatchingId"); + + // Then + assertFalse(result.isPresent()); + } + + @Test + public void addCaptchaRelatedFieldsToPageContext_recaptchaImpl() throws IOException { + // Given + props.setProperty("captcha.implementation", "RECAPTCHAv2"); + Map context = new HashMap<>(); + + // When + CaptchaServiceBean.addCaptchaRelatedFieldsToPageContext(context); + + // Then + assertEquals("RECAPTCHAV2", context.get("captchaToUse")); + } + + @Test + public void addCaptchaRelatedFieldsToPageContext_nanocaptchaImpl() throws IOException { + // Given + props.setProperty("captcha.implementation", "NANOCAPTCHA"); + Map context = new HashMap<>(); + + // When + CaptchaServiceBean.addCaptchaRelatedFieldsToPageContext(context); + + // Then + assertEquals("NANOCAPTCHA", context.get("captchaToUse")); + } + + @Test + public void getCaptchaImpl_EnabledCaptcha() { + // Given + props.setProperty("captcha.enabled", "true"); + props.setProperty("captcha.implementation", "RECAPTCHAv2"); + + // When + CaptchaImplementation captchaImpl = CaptchaServiceBean.getCaptchaImpl(); + + // Then + assertEquals(CaptchaImplementation.RECAPTCHAV2, captchaImpl); + } + + @Test + public void getCaptchaImpl_DisabledCaptcha() { + // Given + props.setProperty("captcha.enabled", "false"); + + // When + CaptchaImplementation captchaImpl = CaptchaServiceBean.getCaptchaImpl(); + + // Then + assertEquals(CaptchaImplementation.NONE, captchaImpl); + } + + @Test + public void getCaptchaImpl_DefaultImplementation() { + // Given + props.setProperty("captcha.enabled", "true"); + props.setProperty("captcha.implementation", null); + + // When + CaptchaImplementation captchaImpl = CaptchaServiceBean.getCaptchaImpl(); + + // Then + assertEquals(CaptchaImplementation.NANOCAPTCHA, captchaImpl); + } + + @Test + public void validateCaptcha_NoneValid() { + // Given + props.setProperty("captcha.enabled", "false"); + + // Act + boolean result = CaptchaServiceBean.validateCaptcha("anyInput", "anyChallengeId"); + + // Then + assertTrue(result); + } +} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/NanocaptchaProviderTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/NanocaptchaProviderTest.java new file mode 100644 index 0000000000..7128e97c00 --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/NanocaptchaProviderTest.java @@ -0,0 +1,129 @@ +package edu.cornell.mannlib.vitro.webapp.service; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaBundle; +import edu.cornell.mannlib.vitro.webapp.beans.CaptchaServiceBean; +import edu.cornell.mannlib.vitro.webapp.beans.NanocaptchaProvider; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import org.junit.Before; +import org.junit.Test; +import stubs.edu.cornell.mannlib.vitro.webapp.config.ConfigurationPropertiesStub; +import stubs.javax.servlet.ServletContextStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import stubs.javax.servlet.http.HttpSessionStub; + +public class NanocaptchaProviderTest { + + private final ConfigurationPropertiesStub props = new ConfigurationPropertiesStub(); + + private final NanocaptchaProvider provider = new NanocaptchaProvider(); + + + @Before + public void createConfigurationProperties() { + props.setProperty("captcha.enabled", "true"); + ServletContextStub ctx = new ServletContextStub(); + ConfigurationProperties.setInstance(props); + + HttpSessionStub session = new HttpSessionStub(); + session.setServletContext(ctx); + + HttpServletRequestStub httpServletRequest = new HttpServletRequestStub(); + httpServletRequest.setSession(session); + } + + @Test + public void generateChallenge_ValidEasyChallengeGenerated() throws IOException { + // Given + props.setProperty("nanocaptcha.difficulty", "easy"); + + // When + CaptchaBundle captchaBundle = provider.generateRefreshChallenge(); + + // Then + assertNotNull(captchaBundle); + assertNotNull(captchaBundle.getB64Image()); + assertNotNull(captchaBundle.getCode()); + assertNotNull(captchaBundle.getCaptchaId()); + } + + @Test + public void generateChallenge_ValidEmptyChallengeGenerated() throws IOException { + // Given + props.setProperty("nanocaptcha.difficulty", ""); + + // When + CaptchaBundle captchaBundle = provider.generateRefreshChallenge(); + + // Then + assertNotNull(captchaBundle); + assertNotNull(captchaBundle.getB64Image()); + assertNotNull(captchaBundle.getCode()); + assertNotNull(captchaBundle.getCaptchaId()); + } + + @Test + public void generateChallenge_ValidInvalidDifficultyChallengeGenerated() throws IOException { + // Given + props.setProperty("nanocaptcha.difficulty", "asdasdasd"); + + // When + CaptchaBundle captchaBundle = provider.generateRefreshChallenge(); + + // Then + assertNotNull(captchaBundle); + assertNotNull(captchaBundle.getB64Image()); + assertNotNull(captchaBundle.getCode()); + assertNotNull(captchaBundle.getCaptchaId()); + } + + @Test + public void generateChallenge_ValidHardChallengeGenerated() throws IOException { + // Given + props.setProperty("nanocaptcha.difficulty", "hard"); + + // When + CaptchaBundle captchaBundle = provider.generateRefreshChallenge(); + + // Then + assertNotNull(captchaBundle); + assertNotNull(captchaBundle.getB64Image()); + assertNotNull(captchaBundle.getCode()); + assertNotNull(captchaBundle.getCaptchaId()); + } + + @Test + public void validateCaptcha_NanoCaptchaValid() { + // Given + CaptchaBundle sampleChallenge = new CaptchaBundle("sampleB64Image", "validCode", "challengeId"); + CaptchaServiceBean.getCaptchaChallenges().put("challengeId", sampleChallenge); + props.setProperty("captcha.implementation", "NANOCAPTCHA"); + + // Act + boolean result = provider.validateCaptcha("validCode", "challengeId"); + + // Assert + assertTrue(result); + } + + @Test + public void addCaptchaRelatedFieldsToPageContext_NanocaptchaImpl() throws IOException { + // Given + Map context = new HashMap<>(); + + // When + provider.addCaptchaRelatedFieldsToPageContext(context); + + // Assert + assertNull(context.get("siteKey")); + assertNotNull(context.get("challenge")); + assertNotNull(context.get("challengeId")); + } +} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/Recaptchav2ProviderTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/Recaptchav2ProviderTest.java new file mode 100644 index 0000000000..e165bd3b63 --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/service/Recaptchav2ProviderTest.java @@ -0,0 +1,67 @@ +package edu.cornell.mannlib.vitro.webapp.service; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.beans.Recaptchav2Provider; +import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; +import org.junit.Before; +import org.junit.Test; +import stubs.edu.cornell.mannlib.vitro.webapp.config.ConfigurationPropertiesStub; +import stubs.javax.servlet.ServletContextStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import stubs.javax.servlet.http.HttpSessionStub; + +public class Recaptchav2ProviderTest extends AbstractTestClass { + + private final ConfigurationPropertiesStub props = new ConfigurationPropertiesStub(); + + private final Recaptchav2Provider provider = new Recaptchav2Provider(); + + + @Before + public void createConfigurationProperties() { + props.setProperty("captcha.enabled", "true"); + ServletContextStub ctx = new ServletContextStub(); + ConfigurationProperties.setInstance(props); + + HttpSessionStub session = new HttpSessionStub(); + session.setServletContext(ctx); + + HttpServletRequestStub httpServletRequest = new HttpServletRequestStub(); + httpServletRequest.setSession(session); + } + + @Test + public void validateReCaptcha_InvalidResponse_ReturnsFalse() { + // Given + props.setProperty("recaptcha.secretKey", "WRONG_SECRET_KEY"); + + // When + boolean result = provider.validateReCaptcha("invalidResponse"); + + // Then + assertFalse(result); + } + + @Test + public void addCaptchaRelatedFieldsToPageContext_RecaptchaImpl() throws IOException { + // Given + props.setProperty("recaptcha.siteKey", "SITE_KEY"); + Map context = new HashMap<>(); + + // When + provider.addCaptchaRelatedFieldsToPageContext(context); + + // Assert + assertNotNull(context.get("siteKey")); + assertNull(context.get("challenge")); + assertNull(context.get("challengeId")); + } +} diff --git a/home/src/main/resources/config/example.runtime.properties b/home/src/main/resources/config/example.runtime.properties index 989cf19e3f..024ddc611f 100644 --- a/home/src/main/resources/config/example.runtime.properties +++ b/home/src/main/resources/config/example.runtime.properties @@ -194,3 +194,16 @@ proxy.eligibleTypeList = http://www.w3.org/2002/07/owl#Thing #fileUpload.maxFileSize = 10485760 #comma separated list of mime types allowed for upload #fileUpload.allowedMIMETypes = image/png, application/pdf + +# Captcha configuration. Available implementations are: nanocaptcha (text-based) and recaptchav2 +# nanocaptcha is available in 2 difficulties (easy and hard) +# If captcha.implementation property is not provided, system will fall back to nanocaptcha implementation +# with easy diffuculty (if captcha is enabled) +# For recaptchav2 method, you have to provide siteKey and secretKey. +# More information on siteKey and secretKey is available on: https://www.google.com/recaptcha +# Only recaptchav2 implementation is supported +captcha.enabled = true +captcha.implementation = nanocaptcha +nanocaptcha.difficulty = easy +#recaptcha.siteKey = +#recaptcha.secretKey = diff --git a/home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttime/vitro_UiLabel.ttl index ab0b41be4b..ef00eaac44 100644 --- a/home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/de_DE/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Bitte geben Sie ihren vollständigen Namen an."@de-DE ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Bitte geben Sie eine gültige E-Mail-Adresse ein."@de-DE ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Bitte geben Sie Ihre Kommentare oder Fragen in das bereitgestellte Feld ein."@de-DE ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Bitte lösen Sie die CAPTCHA-Herausforderung im bereitgestellten Sicherheitsfeld."@de-DE ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Ungültige CAPTCHA-Lösung, bitte versuchen Sie es erneut."@de-DE ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/en_CA/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/en_CA/interface-i18n/firsttime/vitro_UiLabel.ttl index d4a455c232..eacfae4b4c 100644 --- a/home/src/main/resources/rdf/i18n/en_CA/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/en_CA/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please enter a value in the Full name field."@en-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please enter a valid email address."@en-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please enter your comments or questions in the space provided."@en-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please solve CAPTCHA challenge in the security field provided."@en-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Invalid CAPTCHA solution, please try again."@en-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl index 332b43c651..157ea5bcb0 100644 --- a/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please enter a value in the Full name field."@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please enter a valid email address."@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please enter your comments or questions in the space provided."@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Please solve CAPTCHA challenge in the security field provided."@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Invalid CAPTCHA solution, please try again."@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/vitro_UiLabel.ttl index a2e04ae6cb..3871418990 100644 --- a/home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/es/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, introduzca un valor en el campo de nombre completo."@es ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, introduzca una dirección de correo electrónico válida."@es ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, introduzca sus comentarios o preguntas en el espacio proporcionado."@es ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, resuelva el desafío CAPTCHA en el campo de seguridad proporcionado."@es ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Solución CAPTCHA no válida, por favor, inténtelo de nuevo."@es ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/fr_CA/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/fr_CA/interface-i18n/firsttime/vitro_UiLabel.ttl index ebe0af3875..74a00f2c9c 100644 --- a/home/src/main/resources/rdf/i18n/fr_CA/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/fr_CA/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Veuillez entrer une valeur dans le champ du nom complet."@fr-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Veuillez entrer une adresse courriel valide."@fr-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Veuillez entrer vos commentaires ou questions dans l'espace prévu."@fr-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Veuillez résoudre le défi CAPTCHA dans le champ de sécurité fourni."@fr-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Solution CAPTCHA invalide, veuillez réessayer."@fr-CA ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttime/vitro_UiLabel.ttl index f965878320..4297780456 100644 --- a/home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/pt_BR/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, insira um valor no campo, nome completo."@pt-BR ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, insira um endereço de e-mail válido."@pt-BR ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, insira seus comentários ou perguntas no espaço fornecido."@pt-BR ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Por favor, resolva o desafio CAPTCHA no campo de segurança fornecido."@pt-BR ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "A solução do CAPTCHA informada está inválida, por favor, tente novamente."@pt-BR ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl index ec1510a901..d091845941 100644 --- a/home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/ru_RU/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Пожалуйста, введите Ваше имя."@ru-RU ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Пожалуйста, введите действительный адрес электронной почты."@ru-RU ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Пожалуйста, заполните поле для комментариев, вопросов и предложений."@ru-RU ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Пожалуйста, выполните полностью автоматизированный тест Тьюринга для различения компьютеров и людей."@ru-RU ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Недопустимое решение теста Тьюринга для различения компьютеров и людей. Пожалуйста, попробуйте снова."@ru-RU ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/home/src/main/resources/rdf/i18n/sr_Latn_RS/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/sr_Latn_RS/interface-i18n/firsttime/vitro_UiLabel.ttl index 75cf97d9c6..a900c469f8 100644 --- a/home/src/main/resources/rdf/i18n/sr_Latn_RS/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/sr_Latn_RS/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6269,3 +6269,43 @@ uil-data:no_individual_associated_with_id.Vitro uil:hasApp "Vitro" ; uil:hasKey "no_individual_associated_with_id" ; uil:hasPackage "Vitro-languages" . + +uil-data:full_name_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Molimo vas da unesete vrednost u polje za puno ime."@sr-Latn-RS ; + uil:hasApp "Vitro" ; + uil:hasKey "full_name_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:email_address_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Molimo vas da unesete ispravnu email adresu."@sr-Latn-RS ; + uil:hasApp "Vitro" ; + uil:hasKey "email_address_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:comments_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Molimo vas da unesete vaše komentare ili pitanja u predviđeno polje."@sr-Latn-RS ; + uil:hasApp "Vitro" ; + uil:hasKey "comments_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_empty.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Molimo vas da rešite CAPTCHA izazov u predviđenom sigurnosnom polju."@sr-Latn-RS ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_empty" ; + uil:hasPackage "Vitro-languages" . + +uil-data:captcha_user_sol_invalid.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Pogrešno rešenje CAPTCHA izazova, molimo vas da pokušate ponovo."@sr-Latn-RS ; + uil:hasApp "Vitro" ; + uil:hasKey "captcha_user_sol_invalid" ; + uil:hasPackage "Vitro-languages" . diff --git a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl index 55c0fb688e..34ab812df0 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/contactForm/contactForm-form.ftl @@ -14,25 +14,44 @@

${i18n().interest_thanks(siteName)}

- - - - - +
+ + + + + - - - - - +
+ +
- - +
+
+ +
+
+
+ +
-

-

+ <#if captchaToUse == "RECAPTCHAV2"> +
+ + <#elseif captchaToUse == "NANOCAPTCHA"> + +

+ +

+ + Refresh page if not displayed... +
+
+ + +

+

@@ -51,10 +70,38 @@ ${stylesheets.add('', '')} ${scripts.add('', - '', '')} - + +<#if captchaToUse == "RECAPTCHAV2"> + + +<#elseif captchaToUse == "NANOCAPTCHA"> + + diff --git a/webapp/src/main/webapp/templates/freemarker/edit/forms/css/customForm.css b/webapp/src/main/webapp/templates/freemarker/edit/forms/css/customForm.css index fe05157bf5..9172e67bd8 100644 --- a/webapp/src/main/webapp/templates/freemarker/edit/forms/css/customForm.css +++ b/webapp/src/main/webapp/templates/freemarker/edit/forms/css/customForm.css @@ -113,6 +113,28 @@ form.customForm label.dateTime { fieldset.dateTime select { margin-top: 0; } + +.captcha-container { + position: relative; + display: inline-block; +} + +.captcha-container span { + position: absolute; + top: 0; + right: 0; + background: #ffffffeb; + color: #595b5b; + border: 1px solid #595b5b; + font-size: 0.8em; + padding: 0 8px; +} + +.captcha-container span input { + border: 0; + background: transparent; +} + /* ---------------------------------- */ /* ----- FOR MANAGE PUBLICATIONS ---- */ /* ---------------------------------- */