Commit 37cea195 authored by Rémi Zara's avatar Rémi Zara Committed by glepine1
Browse files

FORMS-713: Add a protected way to test a form when not published

When a form is not published, a link to preview it is offered in the
backoffice which contains a cryptographic token and a timestamp.
By default, the link is valid for 30 minutes.

When accessed via this link, the form is usable but:
 * a warning is displayed a the top of pages
 * responses are not saved
parent f230df71
......@@ -851,6 +851,7 @@ message.mandatory.entry=This field is mandatory.
message.errorUniqueField=This field already exists, please type another one.
message.geolocation.checkAdress=You have to choose an address in the list.
message.numberResponse.label=Number of submissions for this form :
warning.inactive.state.bypassed=This form is inactive. Responses will not be saved.
step.previous=Previous step
step.next=Next step
......
......@@ -851,6 +851,7 @@ message.mandatory.entry=Ce champ est obligatoire.
message.errorUniqueField=Ce champ existe d\u00e9jà, veuillez en saisir un autre.
message.geolocation.checkAdress=Vous devez s\u00e9lectionner un adresse dans la liste propos\u00e9
message.numberResponse.label=Nombre de r\u00e9ponses d\u00e9j\u00e0 post\u00e9es pour ce formulaire :
warning.inactive.state.bypassed=Ce formulaire est inactif. Les réponses ne seront pas enregistrées.
step.previous=Etape pr\u00e9c\u00e9dente
step.next=Etape suivante
......
......@@ -90,6 +90,9 @@ public final class FormsConstants
public static final String MARK_ANONYMIZATION_HELP = "anonymization_help_message";
public static final String MARK_BREADCRUMBS = "breadcrumb_template";
public static final String VALUE_VALIDATOR_LISTEQUESTION_NAME = "forms_listQuestionValidator";
public static final String MARK_TIMESTAMP = "timestamp";
public static final String MARK_INACTIVEBYPASSTOKENS = "inactiveBypassTokens";
// Parameters
public static final String PARAMETER_PAGE = "page";
......@@ -137,6 +140,8 @@ public final class FormsConstants
public static final String PARAMETER_DISPLAYED_QUESTIONS = "displayed_questions";
public static final String PARAMETER_INIT = "init";
public static final String PARAMETER_ID_QUESTION_TO_REMOVE = "id_rm_question";
public static final String PARAMETER_TIMESTAMP = "ts";
public static final String PARAMETER_TOKEN = "token";
public static final String PARAMETER_SELECTED_PANEL = "selected_panel";
public static final String PARAMETER_CURRENT_SELECTED_PANEL = "current_selected_panel";
......@@ -166,6 +171,7 @@ public final class FormsConstants
public static final String PROPERTY_MY_LUTECE_ATTRIBUTES_LIST = "entrytype.myluteceuserattribute.attributes.list";
public static final String CONSTANT_MYLUTECE_ATTRIBUTE_I18N_PREFIX = "forms.entrytype.myluteceuserattribute.attribute.";
public static final String PROPERTY_EXPORT_FORM_DATE_CREATION_FORMAT = "forms.export.formResponse.form.date.creation.format";
public static final String PROPERTY_INACTIVE_BYPASS_DURATION_MILLISECONDS = "forms.inactive.bypass.duration.milliseconds";
// Constants
public static final int DEFAULT_FILTER_VALUE = NumberUtils.INTEGER_MINUS_ONE;
......
/*
* Copyright (c) 2002-2021, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.plugins.forms.util;
import fr.paris.lutece.plugins.forms.business.Form;
import fr.paris.lutece.portal.service.util.CryptoService;
/**
* Utilities for forms
*/
public class FormsUtils
{
private FormsUtils( )
{
// utility class
}
/**
* Get a token to bypass the inactive state of the form
*
* @param form
* the form
* @param strTimestamp
* the timestamp for the start of the bypass period
* @return a token
*/
public static String getInactiveBypassToken( Form form, String strTimestamp )
{
StringBuilder builder = new StringBuilder( );
builder.append( "formId:" ).append( form.getId( ) ).append( ":timestamp:" ).append( strTimestamp );
return CryptoService.hmacSHA256( builder.toString( ) );
}
}
......@@ -34,6 +34,7 @@
package fr.paris.lutece.plugins.forms.web;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
......@@ -71,6 +72,7 @@ import fr.paris.lutece.plugins.forms.service.FormService;
import fr.paris.lutece.plugins.forms.service.entrytype.EntryTypeAutomaticFileReading;
import fr.paris.lutece.plugins.forms.service.upload.FormsAsynchronousUploadHandler;
import fr.paris.lutece.plugins.forms.util.FormsConstants;
import fr.paris.lutece.plugins.forms.util.FormsUtils;
import fr.paris.lutece.plugins.forms.validation.IValidator;
import fr.paris.lutece.plugins.forms.web.breadcrumb.IBreadcrumb;
import fr.paris.lutece.plugins.forms.web.entrytype.DisplayType;
......@@ -91,6 +93,7 @@ import fr.paris.lutece.portal.service.security.LuteceUser;
import fr.paris.lutece.portal.service.security.SecurityService;
import fr.paris.lutece.portal.service.security.UserNotSignedException;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.util.mvc.commons.annotations.Action;
import fr.paris.lutece.portal.util.mvc.commons.annotations.View;
import fr.paris.lutece.portal.util.mvc.xpage.MVCApplication;
......@@ -125,6 +128,7 @@ public class FormXPage extends MVCApplication
private static final String MESSAGE_ERROR_STEP_NOT_FINAL = "forms.error.step.isnot.final";
private static final String MESSAGE_STEP_TITLE = "forms.step.title";
private static final String MESSAGE_SUMMARY_TITLE = "forms.summary.title";
private static final String MESSAGE_WARNING_INACTIVE_STATE_BYPASSED = "forms.warning.inactive.state.bypassed";
/**
* Generated serial id
*/
......@@ -174,6 +178,7 @@ public class FormXPage extends MVCApplication
private Step _currentStep;
private StepDisplayTree _stepDisplayTree;
private IBreadcrumb _breadcrumb;
private boolean _bInactiveStateBypassed;
/**
* Return the default XPage with the list of all available Form
......@@ -325,7 +330,7 @@ public class FormXPage extends MVCApplication
String strPathForm = form.getTitle( );
Map<String, Object> model = getModel( );
if ( form.isActive( ) )
if ( form.isActive( ) || bypassInactiveState( form, request ) )
{
if ( _breadcrumb == null )
{
......@@ -353,6 +358,53 @@ public class FormXPage extends MVCApplication
return xPage;
}
/**
* Does the request contain parameters to bypass the inactive state of the
* form
*
* @param form
* the forme
* @param request
* thre request
* @return <code>true</code> if the request contains valid bypass
* parameters, <code>false</code> otherwise
*/
private boolean bypassInactiveState( Form form, HttpServletRequest request )
{
if ( _bInactiveStateBypassed )
{
return true;
}
String strTimestamp = request.getParameter( FormsConstants.PARAMETER_TIMESTAMP );
String strToken = request.getParameter( FormsConstants.PARAMETER_TOKEN );
if ( StringUtils.isBlank( strToken ) || !StringUtils.isNumeric( strTimestamp ) )
{
return false;
}
String refToken = FormsUtils.getInactiveBypassToken( form, strTimestamp );
if ( !refToken.equals( strToken ) )
{
return false;
}
long now = new Date( ).getTime( );
long timestampAge = now - Long.parseLong( strTimestamp );
if ( timestampAge < 0 )
{
return false;
}
long lBypassDuration = AppPropertiesService
.getPropertyLong( FormsConstants.PROPERTY_INACTIVE_BYPASS_DURATION_MILLISECONDS, 1000L * 60 * 30 ); // Half
// hour
// in
// milliseconds
if ( timestampAge <= lBypassDuration )
{
_bInactiveStateBypassed = true;
return true;
}
return false;
}
/**
* @param form
* The form to display
......@@ -412,6 +464,11 @@ public class FormXPage extends MVCApplication
_stepDisplayTree.getCompositeHtml( request, _formResponseManager.findAllResponses( ), getLocale( request ), DisplayType.EDITION_FRONTOFFICE ) );
model.put( FormsConstants.MARK_FORM_TOP_BREADCRUMB, _breadcrumb.getTopHtml( request, _formResponseManager ) );
model.put( FormsConstants.MARK_FORM_BOTTOM_BREADCRUMB, _breadcrumb.getBottomHtml( request, _formResponseManager ) );
if ( bypassInactiveState( form, request ) )
{
addWarning( MESSAGE_WARNING_INACTIVE_STATE_BYPASSED, getLocale( request ) );
}
fillCommons( model );
}
/**
......@@ -549,7 +606,7 @@ public class FormXPage extends MVCApplication
form.setCurrentNumberResponse( FormHome.getNumberOfResponseForms( form.getId( ) ) );
}
Map<String, Object> model = buildModelForSummary( request );
Map<String, Object> model = buildModelForSummary( form, request );
model.put( FormsConstants.MARK_ID_FORM, form.getId( ) );
model.put( FormsConstants.MARK_FORM, form );
......@@ -579,7 +636,7 @@ public class FormXPage extends MVCApplication
* the request
* @return the model
*/
private Map<String, Object> buildModelForSummary( HttpServletRequest request )
private Map<String, Object> buildModelForSummary( Form form, HttpServletRequest request )
{
Map<String, Object> mapFormResponseSummaryModel = getModel( );
......@@ -587,7 +644,11 @@ public class FormXPage extends MVCApplication
List<String> listStepHtml = buildStepsHtml( request, listValidatedStep );
mapFormResponseSummaryModel.put( MARK_LIST_SUMMARY_STEP_DISPLAY, listStepHtml );
if ( bypassInactiveState( form, request ) )
{
addWarning( MESSAGE_WARNING_INACTIVE_STATE_BYPASSED, getLocale( request ) );
}
fillCommons( mapFormResponseSummaryModel );
return mapFormResponseSummaryModel;
}
......@@ -714,6 +775,10 @@ public class FormXPage extends MVCApplication
Map<String, Object> model = getModel( );
model.put( FormsConstants.PARAMETER_ID_FORM, form.getId( ) );
if ( bypassInactiveState( form, request ) )
{
addWarning( MESSAGE_WARNING_INACTIVE_STATE_BYPASSED, getLocale( request ) );
}
init( request );
......@@ -731,6 +796,7 @@ public class FormXPage extends MVCApplication
}
model.put( FormsConstants.PARAMETER_BACK_URL, strBackUrl );
fillCommons( model );
XPage xPage = getXPage( TEMPLATE_FORM_SUBMITTED, getLocale( request ), model );
xPage.setTitle( form.getTitle( ) );
......@@ -778,7 +844,7 @@ public class FormXPage extends MVCApplication
form = FormHome.findByPrimaryKey( _currentStep.getIdForm( ) );
}
if ( !form.isActive( ) )
if ( !form.isActive( ) && !bypassInactiveState( form, request ) )
{
if ( StringUtils.isNotEmpty( form.getUnavailableMessage( ) ) )
{
......@@ -1271,6 +1337,10 @@ public class FormXPage extends MVCApplication
*/
private void saveFormResponse( Form form, HttpServletRequest request ) throws SiteMessageException
{
if ( _bInactiveStateBypassed )
{
return; // form was in testing mode; do not save response
}
FormResponse formResponse = _formResponseManager.getFormResponse( );
if ( form.isAuthentificationNeeded( ) )
{
......@@ -1370,6 +1440,7 @@ public class FormXPage extends MVCApplication
_currentStep = null;
_stepDisplayTree = null;
_breadcrumb = null;
_bInactiveStateBypassed = false;
FormsAsynchronousUploadHandler.getHandler( ).removeSessionFiles( request.getSession( ) );
}
......
......@@ -34,9 +34,11 @@
package fr.paris.lutece.plugins.forms.web;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
......@@ -47,6 +49,8 @@ import fr.paris.lutece.plugins.forms.business.Form;
import fr.paris.lutece.plugins.forms.business.FormAction;
import fr.paris.lutece.plugins.forms.business.FormActionHome;
import fr.paris.lutece.plugins.forms.business.FormHome;
import fr.paris.lutece.plugins.forms.util.FormsConstants;
import fr.paris.lutece.plugins.forms.util.FormsUtils;
import fr.paris.lutece.portal.business.right.Right;
import fr.paris.lutece.portal.business.right.RightHome;
import fr.paris.lutece.portal.business.user.AdminUser;
......@@ -105,10 +109,15 @@ public class FormsDashboardComponent extends DashboardComponent
List<FormAction> listAuthorisedActions = (List<FormAction>) RBACService.getAuthorizedActionsCollection( listFormActions, form, (User) user );
form.setActions( listAuthorisedActions );
}
String strTimespamp = Long.toString( new Date( ).getTime( ) );
Map<String, String> formIdToToken = displayList.stream( ).filter( f -> !f.isActive( ) )
.collect( Collectors.toMap( f -> Integer.toString( f.getId( ) ), f -> FormsUtils.getInactiveBypassToken( f, strTimespamp ) ) );
Map<String, Object> model = new HashMap<>( );
model.put( MARK_FORM_LIST, displayList );
model.put( MARK_URL, url.getUrl( ) );
model.put( FormsConstants.MARK_TIMESTAMP, strTimespamp );
model.put( FormsConstants.MARK_INACTIVEBYPASSTOKENS, formIdToToken );
HtmlTemplate t = AppTemplateService.getTemplate( TEMPLATE_DASHBOARD, user.getLocale( ), model );
......
......@@ -33,10 +33,12 @@
*/
package fr.paris.lutece.plugins.forms.web.admin;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
......@@ -61,6 +63,7 @@ import fr.paris.lutece.plugins.forms.service.FormService;
import fr.paris.lutece.plugins.forms.service.FormsResourceIdService;
import fr.paris.lutece.plugins.forms.service.json.FormJsonService;
import fr.paris.lutece.plugins.forms.util.FormsConstants;
import fr.paris.lutece.plugins.forms.util.FormsUtils;
import fr.paris.lutece.plugins.forms.web.breadcrumb.BreadcrumbManager;
import fr.paris.lutece.plugins.genericattributes.business.GenAttFileItem;
import fr.paris.lutece.portal.business.file.File;
......@@ -240,6 +243,10 @@ public class FormJspBean extends AbstractJspBean
}
String strTimespamp = Long.toString( new Date( ).getTime( ) );
Map<String, String> formIdToToken = paginator.getPageItems( ).stream( ).filter( f -> !f.isActive( ) )
.collect( Collectors.toMap( f -> Integer.toString( f.getId( ) ), f -> FormsUtils.getInactiveBypassToken( f, strTimespamp ) ) );
model.put( MARK_PAGINATOR, paginator );
model.put( MARK_NB_ITEMS_PER_PAGE, EMPTY_STRING + _nItemsPerPage );
......@@ -248,6 +255,8 @@ public class FormJspBean extends AbstractJspBean
model.put( MARK_PERMISSION_CREATE_FORMS,
RBACService.isAuthorized( Form.RESOURCE_TYPE, RBAC.WILDCARD_RESOURCES_ID, FormsResourceIdService.PERMISSION_CREATE, (User) adminUser ) );
model.put( MARK_IS_ACTIVE_KIBANA_FORMS_PLUGIN, PluginService.isPluginEnable( KIBANA_FORMS_PLUGIN_NAME ) );
model.put( FormsConstants.MARK_TIMESTAMP, strTimespamp );
model.put( FormsConstants.MARK_INACTIVEBYPASSTOKENS, formIdToToken );
setPageTitleProperty( EMPTY_STRING );
......
......@@ -28,3 +28,6 @@ forms.export.pdf.zip=false
# Duration in minutes of the validity of generated url for file download (if 0, the links will be always valid)
forms.file.download.validity=0
# Duration in milliseconds of the validity of the link to test a form that is not active
#forms.inactive.bypass.duration.milliseconds=1800000
......@@ -34,8 +34,13 @@
</#list>
</#if>
<#if form.active>
<@aButton href='jsp/site/Portal.jsp?page=forms&view=stepView&id_form=${form.id}&init=true' title='#i18n{forms.manageForm.FOLink.label} ${form.title}' hideTitle=['all'] params='target="_blank"' buttonIcon='external-link' size='sm' color='success' />
<#assign inactiveBypass=''>
<#assign color='success'>
<#else>
<#assign inactiveBypass='&ts='+timestamp+'&token='+inactiveBypassTokens[form.id?string]>
<#assign color='warning'>
</#if>
<@aButton href='jsp/site/Portal.jsp?page=forms&view=stepView&id_form=${form.id}&init=true${inactiveBypass}' title='#i18n{forms.manageForm.FOLink.label} ${form.title}' hideTitle=['all'] params='target="_blank"' buttonIcon='external-link' size='sm' color=color />
</@td>
</@tr>
......
......@@ -71,8 +71,13 @@
</#list>
</#if>
<#if form.active>
<@aButton href='jsp/site/Portal.jsp?page=forms&view=stepView&id_form=${form.id}&init=true' title='#i18n{forms.manageForm.FOLink.label} ${form.title}' hideTitle=['all'] params='target="_blank"' buttonIcon='external-link' size='sm' color='success' />
<#assign inactiveBypass=''>
<#assign color='success'>
<#else>
<#assign inactiveBypass='&ts='+timestamp+'&token='+inactiveBypassTokens[form.id?string]>
<#assign color='warning'>
</#if>
<@aButton href='jsp/site/Portal.jsp?page=forms&view=stepView&id_form=${form.id}&init=true${inactiveBypass}' title='#i18n{forms.manageForm.FOLink.label} ${form.title}' hideTitle=['all'] params='target="_blank"' buttonIcon='external-link' size='sm' color=color />
<#if is_active_kibana_forms_plugin>
<@aButton href='jsp/admin/plugins/kibana/KibanaDashboard.jsp?view=dashboard&tab=FormsDataSource_${form.id}' title='#i18n{forms.manageForm.stats.label}' hideTitle=['all'] params='target="_blank"' buttonIcon='chart-area' size='sm' color='info' />
</#if>
......
<@messages warnings=warnings />
<#if messageInfo?? >
${messageInfo}
</#if>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment