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>
@@ -51,10 +70,38 @@
${stylesheets.add('',
'')}
${scripts.add('',
- '',
'')}
-
+
+<#if captchaToUse == "RECAPTCHAV2">
+
+
+<#elseif captchaToUse == "NANOCAPTCHA">
+
+#if>
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 ---- */
/* ---------------------------------- */