Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,46 @@ The studio-frontend has been migrated into the new studio-pnpm-workspace structu
The form-editor-studio-plugin can still be part of this external extensions repo and does not need to be moved into `apps/studio-client/apps/main/extensions/`. But it needs to be registered as an extension with the extensions-tool: Call `mvn extensions:sync -Denable=core-forms` in the folder `workspace-configuration/extensions`

## Integrate the Code in your CoreMedia Blueprint Workspace
You can integrate the extension in three ways:
You can integrate the extension in three ways. For all of them, you have to keep in mind:
* The formEditor uses maven-dependencies of the blueprint-workspace, keep the versions and groupIds in sync, e.g. "1-SNAPSHOT" and "com.coremedia.blueprint"
* After integrating the code, use the extension tool in the root folder of the project to link the modules into your workspace:
```
mvn -f workspace-configuration/extensions com.coremedia.maven:extensions-maven-plugin:LATEST:sync -Denable=core-forms
```


**1. Git SubModule**

Add this repo or your fork as a Git Submodule to your existing CoreMedia Blueprint-Workspace in the extensions-folder.

This way, you will be able to merge new commits made in this repo back to your fork.

This is the recommended approach because you will also be able to develop quickly, performing a make on the sources with a running studio- or cae-webapp.
Use this approach if you are planing to contribute changes to the GitHub-Repo.

From the project's root folder, clone this repository as submodule into the extensions folder. Make sure to use the branch name that matches your workspace version.
```
git submodule add https://github.com/tallence/core-forms.git modules/extensions/core-forms
```

- Use the extension tool in the root folder of the project to link the modules into your workspace.
```
mvn -f workspace-configuration/extensions com.coremedia.maven:extensions-maven-plugin:LATEST:sync -Denable=core-forms
```

**2. Copy files**
**2. Git Subtree**

Download the repo and copy the files into your Blueprint-Workspace Extension-Folder.
Add this repo or your fork as a Git Subtree to your existing CoreMedia Blueprint-Workspace in the extensions-folder.
Use this approach if you are using the GitHub-Repo in a readOnly mode.

To include the subtree into your project the following command line should be executed in the project root directory.

```git subtree add --prefix modules/extensions/core-forms https://github.com/tallence/core-forms.git master --squash```

Further information about git subtree in relation to the github repository can be found here:

This way you won't be able to merge new commits made in this repo back to yours. But if you do not like Git Submodules, you don't have to deal with them.
[Github Subtree-Documentation](https://gist.github.com/SKempin/b7857a6ff6bddb05717cc17a44091202)

However the formeditor uses maven-dependencies of the blueprint-workspace, so you have to keep the versions in sync.

**3. Copy files**

Download the repo and copy the files into your Blueprint-Workspace Extension-Folder.

This way you won't be able to merge new commits made in this repo back to yours. So this way is not recommended.


## Required modifications
To getting started you need to make some modifications and write some code:
Expand Down
1 change: 0 additions & 1 deletion form-editor-cae/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import com.coremedia.blueprint.common.services.context.CurrentContextService;
import com.tallence.formeditor.FormEditorHelper;
import com.tallence.formeditor.FormElementFactory;
import com.tallence.formeditor.cae.handler.ReCaptchaService;
import com.tallence.formeditor.cae.handler.CaptchaService;
import com.tallence.formeditor.contentbeans.FormEditor;
import com.tallence.formeditor.elements.FormElement;

Expand All @@ -33,12 +33,12 @@ public class FormFreemarkerFacade {

private final FormElementFactory formElementFactory;

private final ReCaptchaService reCaptchaService;
private final CaptchaService captchaService;

private final CurrentContextService currentContextService;

public FormFreemarkerFacade(FormElementFactory formElementFactory, ReCaptchaService pReCaptchaService, CurrentContextService currentContextService) {
this.reCaptchaService = pReCaptchaService;
public FormFreemarkerFacade(FormElementFactory formElementFactory, CaptchaService pCaptchaService, CurrentContextService currentContextService) {
this.captchaService = pCaptchaService;
this.formElementFactory = formElementFactory;
this.currentContextService = currentContextService;
}
Expand All @@ -48,6 +48,6 @@ public List<FormElement<?>> parseFormElements(FormEditor formEditor) {
}

public String getReCaptchaWebsiteSecretForSite() {
return reCaptchaService.getWebsiteSecretForSite(currentContextService.getContext());
return captchaService.getWebsiteSecretForSite(currentContextService.getContext());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 Tallence AG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tallence.formeditor.cae.config;

import com.tallence.formeditor.cae.handler.CaptchaService;
import com.tallence.formeditor.cae.handler.ReCaptchaServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FormCaeConfiguration {

@Bean
@ConditionalOnMissingBean(CaptchaService.class)
public ReCaptchaServiceImpl captchaService(@Value("${google.reCaptcha.website-secret}") String websiteSecret,
@Value("${google.reCaptcha.server-secret}") String serverSecret) {

var auth = new ReCaptchaServiceImpl.ReCaptchaAuthentication(websiteSecret, serverSecret);

return new ReCaptchaServiceImpl(auth);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@
package com.tallence.formeditor.cae.handler;

import com.coremedia.blueprint.common.contentbeans.CMNavigation;
import org.springframework.util.MultiValueMap;

/**
* Encapsulates requests to google reCaptcha.
* The impl from CoreMedia elastic social Extension might be used.
* Encapsulates requests to a captcha service.
*/
public interface ReCaptchaService {
public interface CaptchaService {

/**
* Resolves the website-secret for google reCaptcha for the given Context.
* Resolves the website-secret for the captcha for the given Context.
*
* The context is currently ignored. Use it if you need multiple secrets for your sites.
*/
String getWebsiteSecretForSite(CMNavigation currentContext);


/**
* Asks the Google Recaptcha Service, if the given googleReCaptchaResponse is valid.
* @param googleReCaptchaResponse token, generated by the reCaptcha widget, by using the public key
* @return true, if the google reCaptcha service validates the request, false otherwise.
* Asks the Captcha Service, if the given response is valid.
* @param postData contains the captcha token, generated by the frontend widget
* @return true, if the captcha service validates the request, false otherwise.
*/
boolean isHuman(String googleReCaptchaResponse, CMNavigation currentContext);
boolean isHuman(MultiValueMap<String, String> postData, CMNavigation currentContext);

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public class FormController {

private final List<FormAction> formActions;
private final DefaultFormAction defaultFormAction;
private final ReCaptchaService recaptchaService;
private final CaptchaService captchaService;
private final FormFreemarkerFacade formFreemarkerFacade;
private final CurrentContextService currentContextService;
private final RequestMessageSource messageSource;
Expand All @@ -87,15 +87,15 @@ public class FormController {

public FormController(List<FormAction> formActions,
DefaultFormAction defaultFormAction,
ReCaptchaService recaptchaService,
CaptchaService captchaService,
FormFreemarkerFacade formFreemarkerFacade,
CurrentContextService currentContextService,
RequestMessageSource messageSource,
ResourceBundleInterceptor pageResourceBundlesInterceptor,
@Value("${formEditor.cae.encodeData:true}") boolean encodeFormData) {
this.formActions = formActions;
this.defaultFormAction = defaultFormAction;
this.recaptchaService = recaptchaService;
this.captchaService = captchaService;
this.formFreemarkerFacade = formFreemarkerFacade;
this.currentContextService = currentContextService;
this.messageSource = messageSource;
Expand Down Expand Up @@ -165,8 +165,8 @@ public FormProcessingResult socialFormAction(@PathVariable(name = "currentContex
}

if (target.isSpamProtectionEnabled()) {
if (!isHumanByReCaptcha(target, navigation, postData)) {
LOG.warn("Google reCaptcha detected a bot for Form " + target.getContentId());
if (!isHumanByCaptcha(target, navigation, postData)) {
LOG.warn("CaptchaService {} detected a bot for Form {}", captchaService.getClass().getName(), target.getContentId());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return new FormProcessingResult(
false,
Expand Down Expand Up @@ -249,16 +249,15 @@ private MultipartFile processFileInput(MultipartHttpServletRequest multipartRequ

/**
* Validates the generated response-token in postData
* @param target form with reCaptcha
* @param postData includes the response-parameter, generated by the reCaptcha widget
* @param target form with captcha
* @param postData includes the response-parameter, generated by the captcha widget
* @return true, if google says, the token is valid
*/
private boolean isHumanByReCaptcha(FormEditor target, CMChannel currentContext, MultiValueMap<String, String> postData) {
private boolean isHumanByCaptcha(FormEditor target, CMChannel currentContext, MultiValueMap<String, String> postData) {
try {
String googleReCaptchaResponse = postData.get("g-recaptcha-response").get(0);
return recaptchaService.isHuman(googleReCaptchaResponse, currentContext);
return captchaService.isHuman(postData, currentContext);
} catch (Exception ex) {
LOG.error("Failed to verify reCapture via google for form " + target.getContentId(), ex);
LOG.error("Failed to verify captcha via {} for form {}", captchaService.getClass().getName(), target.getContentId(), ex);
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.MultiValueMap;

import java.io.BufferedReader;
import java.io.InputStream;
Expand All @@ -19,7 +20,7 @@
/**
* Encapsulates requests to google reCaptcha.
*/
public class ReCaptchaServiceImpl implements ReCaptchaService {
public class ReCaptchaServiceImpl implements CaptchaService {

private static final Logger LOG = LoggerFactory.getLogger(ReCaptchaServiceImpl.class);

Expand Down Expand Up @@ -53,13 +54,15 @@ private String getServerSecretForSite(CMNavigation currentContext) {
/**
* Asks the Google Recaptcha Service, if the given googleReCaptchaResponse is valid.
*
* @param googleReCaptchaResponse token, generated by the reCaptcha widget, by using the public key
* @param postData contains the token, generated by the reCaptcha widget, by using the public key
* @return true, if the google reCaptcha service validates the request, false otherwise.
*/
public boolean isHuman(String googleReCaptchaResponse, CMNavigation currentContext) {
@Override
public boolean isHuman(MultiValueMap<String, String> postData, CMNavigation currentContext) {

URLConnection connection;
String query;
String googleReCaptchaResponse = postData.get("g-recaptcha-response").get(0);
try {
query = String.format("secret=%s&response=%s",
URLEncoder.encode(getServerSecretForSite(currentContext), CHARSET_UTF_8),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@
</bean>


<bean name="reCaptchaService" class="com.tallence.formeditor.cae.handler.ReCaptchaServiceImpl">
<constructor-arg name="defaultCredentials" ref="reCaptchaDefaultCredentials"/>
</bean>

<bean id="reCaptchaDefaultCredentials" class="com.tallence.formeditor.cae.handler.ReCaptchaServiceImpl$ReCaptchaAuthentication">
<constructor-arg value="${google.reCaptcha.website-secret}"/>
<constructor-arg value="${google.reCaptcha.server-secret}"/>
</bean>

<bean id="formEditorCacheCapacityConfigurer" class="com.coremedia.cache.CacheCapacityConfigurer" init-method="init">
<property name="cache" ref="cache"/>
<property name="capacities">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<bean name="formFreemarkerFacade" class="com.tallence.formeditor.cae.FormFreemarkerFacade">
<constructor-arg name="formElementFactory" ref="formElementFactory"/>
<constructor-arg name="currentContextService" ref="currentContextService"/>
<constructor-arg name="pReCaptchaService" ref="reCaptchaService"/>
<constructor-arg name="pCaptchaService" ref="captchaService"/>
</bean>

<customize:append id="formFreemarkerConfigurerAutoImportsCustomizer" bean="freemarkerConfigurer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
import com.tallence.formeditor.FormElementFactory;
import com.tallence.formeditor.cae.actions.DefaultFormAction;
import com.tallence.formeditor.cae.actions.FormAction;
import com.tallence.formeditor.cae.handler.CaptchaService;
import com.tallence.formeditor.elements.FormElement;
import com.tallence.formeditor.cae.handler.FormConfigController;
import com.tallence.formeditor.cae.handler.FormController;
import com.tallence.formeditor.cae.handler.ReCaptchaService;
import com.tallence.formeditor.cae.handler.ReCaptchaServiceImpl;
import com.tallence.formeditor.parser.AbstractFormElementParser;
import com.tallence.formeditor.cae.serializer.FormElementSerializerFactory;
Expand Down Expand Up @@ -100,18 +100,18 @@ public FormElementFactory formElementFactory(List<AbstractFormElementParser<? ex
}

/**
* Mocking the {@link ReCaptchaService} it is not yet tested.
* Mocking the {@link CaptchaService} it is not yet tested.
*/
@Bean
@Scope(SCOPE_SINGLETON)
public ReCaptchaService reCaptchaService() {
public CaptchaService captchaService() {
return new ReCaptchaServiceImpl(new ReCaptchaServiceImpl.ReCaptchaAuthentication(null, null));
}

@Bean
@Scope(SCOPE_SINGLETON)
public FormFreemarkerFacade freemarkerFacade(FormElementFactory formElementFactory, ReCaptchaService reCaptchaService, CurrentContextService currentContextService) {
return new FormFreemarkerFacade(formElementFactory, reCaptchaService, currentContextService);
public FormFreemarkerFacade freemarkerFacade(FormElementFactory formElementFactory, CaptchaService captchaService, CurrentContextService currentContextService) {
return new FormFreemarkerFacade(formElementFactory, captchaService, currentContextService);
}

@Bean
Expand All @@ -122,13 +122,13 @@ MockMvc mockMvc(WebApplicationContext wac) {
@Bean
FormController formController(List<FormAction> formActions,
DefaultFormAction defaultFormAction,
ReCaptchaService recaptchaService,
CaptchaService captchaService,
FormFreemarkerFacade formFreemarkerFacade,
CurrentContextService currentContextService,
RequestMessageSource messageSource,
ResourceBundleInterceptor pageResourceBundlesInterceptor,
@Value("${formEditor.cae.encodeData:true}") boolean encodeFormData) {
return new FormController(formActions, defaultFormAction, recaptchaService, formFreemarkerFacade, currentContextService, messageSource, pageResourceBundlesInterceptor, encodeFormData);
return new FormController(formActions, defaultFormAction, captchaService, formFreemarkerFacade, currentContextService, messageSource, pageResourceBundlesInterceptor, encodeFormData);
}

@Bean
Expand Down