001/*
002 * Copyright 2021 Anyware Services
003 * 
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 * 
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.forms.content.processing;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.IOException;
021import java.io.InputStreamReader;
022import java.io.Reader;
023import java.sql.Connection;
024import java.sql.PreparedStatement;
025import java.sql.ResultSet;
026import java.sql.SQLException;
027import java.sql.Timestamp;
028import java.sql.Types;
029import java.text.DateFormat;
030import java.text.ParseException;
031import java.text.SimpleDateFormat;
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.Date;
036import java.util.HashMap;
037import java.util.Iterator;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044import java.util.regex.PatternSyntaxException;
045
046import org.apache.avalon.framework.component.Component;
047import org.apache.avalon.framework.context.Context;
048import org.apache.avalon.framework.context.ContextException;
049import org.apache.avalon.framework.context.Contextualizable;
050import org.apache.avalon.framework.service.ServiceException;
051import org.apache.avalon.framework.service.ServiceManager;
052import org.apache.avalon.framework.service.Serviceable;
053import org.apache.cocoon.components.ContextHelper;
054import org.apache.cocoon.environment.ObjectModelHelper;
055import org.apache.cocoon.environment.Request;
056import org.apache.cocoon.servlet.multipart.Part;
057import org.apache.cocoon.servlet.multipart.PartOnDisk;
058import org.apache.cocoon.servlet.multipart.RejectedPart;
059import org.apache.commons.io.IOUtils;
060import org.apache.commons.lang.StringUtils;
061import org.apache.excalibur.source.Source;
062import org.apache.excalibur.source.SourceResolver;
063
064import org.ametys.core.captcha.CaptchaHelper;
065import org.ametys.core.datasource.ConnectionHelper;
066import org.ametys.core.datasource.dbtype.SQLDatabaseTypeExtensionPoint;
067import org.ametys.core.group.InvalidModificationException;
068import org.ametys.core.right.RightManager;
069import org.ametys.core.user.CurrentUserProvider;
070import org.ametys.core.user.UserIdentity;
071import org.ametys.core.util.URIUtils;
072import org.ametys.core.util.mail.SendMailHelper;
073import org.ametys.plugins.forms.content.Field;
074import org.ametys.plugins.forms.content.Field.FieldType;
075import org.ametys.plugins.forms.content.Form;
076import org.ametys.plugins.forms.content.data.FieldValue;
077import org.ametys.plugins.forms.content.jcr.FormPropertiesManager;
078import org.ametys.plugins.forms.content.table.DbTypeHelper;
079import org.ametys.plugins.forms.content.table.FormTableManager;
080import org.ametys.plugins.repository.AmetysObjectResolver;
081import org.ametys.plugins.repository.UnknownAmetysObjectException;
082import org.ametys.plugins.workflow.store.JdbcWorkflowStore;
083import org.ametys.plugins.workflow.support.WorkflowHelper;
084import org.ametys.plugins.workflow.support.WorkflowProvider;
085import org.ametys.runtime.config.Config;
086import org.ametys.runtime.i18n.I18nizableText;
087import org.ametys.runtime.plugin.component.AbstractLogEnabled;
088import org.ametys.web.AnalyseFileForVirusHelper;
089import org.ametys.web.URIPrefixHandler;
090import org.ametys.web.repository.page.Page;
091import org.ametys.web.repository.site.Site;
092import org.ametys.web.repository.site.SiteManager;
093
094import com.opensymphony.workflow.InvalidActionException;
095import com.opensymphony.workflow.Workflow;
096import com.opensymphony.workflow.WorkflowException;
097
098import jakarta.mail.MessagingException;
099
100/**
101 * Helper that processes the user submitted data on a form.
102 */
103public class ProcessFormHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
104{
105    /** The Avalon role */
106    public static final String ROLE = ProcessFormHelper.class.getName();
107    
108    /** The form ID parameter. */
109    public static final String PARAM_FORM_ID = "ametys-form-id";
110
111    /** The content ID parameter. */
112    protected static final String PARAM_CONTENT_ID = "ametys-content-id";
113
114    /** The integer validation pattern. */
115    protected static final Pattern _INT_PATTERN = Pattern.compile(FormValidators.getIntegerPattern());
116
117    /** The integer validation pattern. */
118    protected static final Pattern _FLOAT_PATTERN = Pattern.compile(FormValidators.getFloatPattern());
119
120    /** The integer validation pattern. */
121    protected static final Pattern _DATE_PATTERN = Pattern.compile(FormValidators.getDatePattern());
122
123    /** The integer validation pattern. */
124    protected static final Pattern _TIME_PATTERN = Pattern.compile(FormValidators.getTimePattern());
125
126    /** The integer validation pattern. */
127    protected static final Pattern _DATETIME_PATTERN = Pattern.compile(FormValidators.getDateTimePattern());
128
129    /** The email validation pattern. */
130    protected static final Pattern _EMAIL_PATTERN = SendMailHelper.EMAIL_VALIDATION;
131
132    /** The phone validation pattern. */
133    protected static final Pattern _PHONE_PATTERN = Pattern.compile(FormValidators.getPhonePattern());
134
135    /** The date format pattern. */
136    protected static final DateFormat _DATE_FORMAT = new SimpleDateFormat(FormValidators.getDateFormat());
137
138    /** The time format pattern. */
139    protected static final DateFormat _TIME_FORMAT = new SimpleDateFormat(FormValidators.getTimeFormat());
140
141    /** The date and time format pattern. */
142    protected static final DateFormat _DATETIME_FORMAT = new SimpleDateFormat(FormValidators.getDateTimeFormat());
143    
144    private static final String __FORM_ENTRY_PATTERN = "${form}";
145    
146    private static Pattern __OPTION_INDEX = Pattern.compile("^option-([0-9]+)-value$");
147    
148    /** Form properties manager. */
149    protected FormPropertiesManager _formPropertiesManager;
150
151    /** Form table manager. */
152    protected FormTableManager _formTableManager;
153
154    /** The source resolver. */
155    protected SourceResolver _sourceResolver;
156
157    /** The ametys object resolver */
158    protected AmetysObjectResolver _ametysObjectResolver;
159
160    /** The site manager. */
161    protected SiteManager _siteManager;
162
163    /** The plugin name. */
164    protected String _pluginName;
165    
166    /** The URI prefix handler */
167    protected URIPrefixHandler _prefixHandler;
168    
169    /** The workflow provider */
170    protected WorkflowProvider _workflowProvider;
171    
172    /** The workflow helper component */
173    protected WorkflowHelper _workflowHelper;
174    
175    /** The SQLDatabaseTypeExtensionPoint instance */
176    protected SQLDatabaseTypeExtensionPoint _sqlDatabaseTypeExtensionPoint;
177    
178    /** The context */
179    protected Context _context;
180
181    /** Rights manager */
182    protected RightManager _rightManager;
183    
184    /** current user provider */
185    protected CurrentUserProvider _currentUserProvied;
186    
187    /** The analyse of files for virus helper */
188    protected AnalyseFileForVirusHelper _analyseFileForVirusHelper;
189
190    
191    @Override
192    public void contextualize(Context context) throws ContextException
193    {
194        _context = context;
195    }
196    
197    @Override
198    public void service(ServiceManager serviceManager) throws ServiceException
199    {
200        _formPropertiesManager = (FormPropertiesManager) serviceManager.lookup(FormPropertiesManager.ROLE);
201        _formTableManager = (FormTableManager) serviceManager.lookup(FormTableManager.ROLE);
202        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
203        _ametysObjectResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
204        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
205        _sqlDatabaseTypeExtensionPoint = (SQLDatabaseTypeExtensionPoint) serviceManager.lookup(SQLDatabaseTypeExtensionPoint.ROLE);
206        _prefixHandler = (URIPrefixHandler) serviceManager.lookup(URIPrefixHandler.ROLE);
207        _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE);
208        _workflowHelper = (WorkflowHelper) serviceManager.lookup(WorkflowHelper.ROLE);
209        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
210        _currentUserProvied = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
211        _analyseFileForVirusHelper = (AnalyseFileForVirusHelper) serviceManager.lookup(AnalyseFileForVirusHelper.ROLE);
212    }
213
214    /**
215     * Process form
216     * @param form the form
217     * @param site the site
218     * @param pluginName the plugin name
219     * @return the form informations
220     * @throws Exception if an error occurred
221     */
222    public FormInformations processForm(Form form, Site site, String pluginName) throws Exception
223    {
224        FormInformations formInformations = new FormInformations();
225        
226        Map objectModel = ContextHelper.getObjectModel(_context);
227        Request request = ObjectModelHelper.getRequest(objectModel);
228        _pluginName = pluginName;
229        
230        FormErrors errors = new FormErrors(form, new LinkedHashMap<String, List<I18nizableText>>());
231        formInformations.setFormErrors(errors);
232        
233        Map<String, FieldValue> input = _getInput(form, request, errors);
234
235        _validateInput(form, input, errors, request);
236        
237        int totalSubmissions = 0;
238
239        if (errors.hasErrors())
240        {
241            return formInformations;
242        }
243         
244        String entryId = null;
245        if (!StringUtils.isEmpty(form.getLimit()))
246        {
247            synchronized (this)
248            { 
249                totalSubmissions = _formTableManager.getTotalSubmissions(form.getId());
250
251                if (Integer.parseInt(form.getLimit()) <= totalSubmissions)
252                {
253                    errors.setLimitReached(true);
254                    return formInformations;
255                }
256                entryId = _insertInput(form, input, objectModel);
257                if (StringUtils.isBlank(entryId))
258                {
259                    errors.setInsertionFailed(true);
260                    return formInformations;
261                }
262            }
263            
264        }
265        else
266        {
267            entryId = _insertInput(form, input, objectModel);
268            if (StringUtils.isBlank(entryId))
269            {
270                errors.setInsertionFailed(true);
271                return formInformations;
272            }
273        }
274        
275        _sendEmails(form, input, site, totalSubmissions);
276
277        formInformations.setRedirection(_getFormRedirection(form, entryId));
278        
279        return formInformations;
280    }
281    
282    /**
283     * Get the form redirection
284     * @param form the form 
285     * @param entryId the entryId
286     * @return the form redirection url
287     */
288    protected String _getFormRedirection(Form form, String entryId)
289    {
290        if (StringUtils.isNotEmpty(form.getRedirectTo()))
291        {
292            String pageId = form.getRedirectTo();
293            try
294            {
295                Page page = _ametysObjectResolver.resolveById(pageId);
296                return _prefixHandler.getAbsoluteUriPrefix(page.getSiteName()) + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + ".html";
297                
298            }
299            catch (UnknownAmetysObjectException e)
300            {
301                getLogger().warn("The form '" + form.getId() + "' wants to redirect to the unexisting page '" + pageId + "'. Redirecting to default page.", e);
302            }
303        }
304        
305        return null;
306    }
307    
308    /**
309     * Get the user input.
310     * 
311     * @param form the Form object.
312     * @param request the user request.
313     * @param errors the input errors.
314     * @return the user data as a Map of column name -&gt; column entry.
315     */
316    protected Map<String, FieldValue> _getInput(Form form, Request request, FormErrors errors)
317    {
318        Map<String, FieldValue> entries = new LinkedHashMap<>();
319
320        // For each field declared in the form,
321        for (Field field : form.getFields())
322        {
323            final String id = field.getId();
324            final String name = field.getName();
325
326            FieldValue entry = null;
327
328            switch (field.getType())
329            {
330                case TEXT:
331                case COST:
332                case HIDDEN:
333                case PASSWORD:
334                    String sValue = (String) request.get(name);
335                    entry = new FieldValue(id, Types.VARCHAR, sValue, field);
336                    break;
337                case SELECT:
338                    String[] values = request.getParameterValues(name);
339                    sValue = values == null ? "" : StringUtils.join(values, "\n");
340                    entry = new FieldValue(id, Types.VARCHAR, sValue, field);
341                    break;
342                case TEXTAREA:
343                    sValue = (String) request.get(name);
344                    entry = new FieldValue(id, Types.LONGVARCHAR, sValue, field);
345                    break;
346                case RADIO:
347                    if (!entries.containsKey(name))
348                    {
349                        sValue = (String) request.get(name);
350                        entry = new FieldValue(name, Types.VARCHAR, sValue, field);
351                    }
352                    else
353                    {
354                        // The value exists, clone it, concatenating the label.
355                        if (StringUtils.isNotEmpty(field.getLabel()))
356                        {
357                            Field radioField = entries.get(name).getField();
358                            Field dummyField = new Field(radioField.getId(), radioField.getType(), radioField.getName(), radioField.getLabel() + "/" + field.getLabel(),
359                                    radioField.getProperties());
360                            entries.get(name).setField(dummyField);
361                        }
362                    }
363                    break;
364                case CHECKBOX:
365                    boolean bValue = request.get(name) != null;
366                    entry = new FieldValue(id, Types.BOOLEAN, bValue, field);
367                    break;
368                case FILE:
369                    entry = _getFileEntry(request, field, id, name, errors);
370                    break;
371                case CAPTCHA:
372                    final String formId = request.getParameter(PARAM_FORM_ID);
373                    final String contentId = request.getParameter(PARAM_CONTENT_ID);
374
375                    final String encodedName = contentId + "%20" + formId + "%20" + field.getId();
376
377                    final String captchaValue = request.getParameter(encodedName);
378                    final String captchaKey = request.getParameter(encodedName + "-key");
379
380                    entry = new FieldValue(id, Types.OTHER, new String[] {captchaValue, captchaKey}, field);
381                    break;
382                default:
383                    break;
384            }
385
386            if (entry != null)
387            {
388                entries.put(entry.getColumnName(), entry);
389            }
390        }
391
392        return entries;
393    }
394
395    /**
396     * Get a file entry from the request.
397     * 
398     * @param request the user request.
399     * @param field the field.
400     * @param id the entry ID.
401     * @param name the field name.
402     * @param errors the form errors.
403     * @return the file entry.
404     */
405    protected FieldValue _getFileEntry(Request request, Field field, String id, String name, FormErrors errors)
406    {
407        FieldValue entry = null;
408
409        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_FILE_";
410
411        Part part = (Part) request.get(name);
412        if (part instanceof RejectedPart)
413        {
414            errors.addError(field.getId(), new I18nizableText("plugin." + _pluginName, keyPrefix + "REJECTED"));
415        }
416        else
417        {
418            PartOnDisk uploadedFilePart = (PartOnDisk) part;
419            if (uploadedFilePart != null)
420            {
421                entry = new FieldValue(id, Types.BLOB, uploadedFilePart.getFile(), field);
422            }
423            else
424            {
425                entry = new FieldValue(id, Types.BLOB, null, field);
426            }
427        }
428
429        return entry;
430    }
431
432    /**
433     * Insert the user submission in the database.
434     * 
435     * @param form the Form object.
436     * @param input the user input.
437     * @param objectModel The object model
438     * @return the entry id if the insertion has succeed, null in case of error
439     * @throws WorkflowException if an exception occurs while initializing a workflow instance for a form entry
440     * @throws InvalidModificationException If an error occurs
441     */
442    protected String _insertInput(Form form, Map<String, FieldValue> input, Map objectModel) throws WorkflowException, InvalidModificationException
443    {
444        boolean success = true;
445        String entryId = null;
446        
447        final String tableName = FormTableManager.TABLE_PREFIX + form.getId();
448
449        Connection connection = null;
450        PreparedStatement stmt = null;
451        
452        try
453        {
454            String dataSourceId = Config.getInstance().getValue(FormTableManager.FORMS_POOL_CONFIG_PARAM);
455            connection = ConnectionHelper.getConnection(dataSourceId);
456            
457            String dbType = ConnectionHelper.getDatabaseType(connection);
458            
459            List<String> columns = new ArrayList<>();
460            List<String> values = new ArrayList<>();
461
462            if (DbTypeHelper.insertIdentity(dbType))
463            {
464                columns.add("id");
465                
466                if (ConnectionHelper.DATABASE_ORACLE.equals(dbType))
467                {
468                    values.add("seq_" + form.getId() + ".nextval");
469                }
470                else
471                {
472                    values.add("?");
473                }
474            }
475            
476            // creation date
477            columns.add(FormTableManager.CREATION_DATE_FIELD);
478            values.add("?");
479
480            // login
481            columns.add(FormTableManager.LOGIN_FIELD);
482            values.add("?");
483
484            // populationId
485            columns.add(FormTableManager.POPULATION_ID_FIELD);
486            values.add("?");
487            
488            Iterator<FieldValue> entries = _getEntriesToInsert(input.values()).iterator();
489            while (entries.hasNext())
490            {
491                FieldValue entry = entries.next();
492                String colName = entry.getColumnName();
493                columns.add(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, colName));
494                values.add("?");
495
496                if (entry.getType() == Types.BLOB)
497                {
498                    String fileNameColumn = colName + FormTableManager.FILE_NAME_COLUMN_SUFFIX;
499                    String normalizedName = DbTypeHelper.normalizeName(dbType, fileNameColumn);
500                    
501                    columns.add(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, normalizedName));
502                    values.add("?");
503                }
504            }
505            
506            if (_formTableManager.hasWorkflowIdColumn(form.getId()))
507            {
508                // Ensure compatibility with form entries that remained without workflow
509                columns.add(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, FormTableManager.WORKFLOW_ID_FIELD));
510                values.add("?");
511            }
512
513            String sql = new StringBuilder()
514                .append("INSERT INTO ")
515                .append(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, tableName))
516                .append(" (")
517                .append(StringUtils.join(columns, ", "))
518                .append(") VALUES (")
519                .append(StringUtils.join(values, ", "))
520                .append(")")
521                .toString();
522
523            if (getLogger().isDebugEnabled())
524            {
525                getLogger().debug("Inserting a user submission in the database :\n" + sql);
526            }
527
528            stmt = connection.prepareStatement(sql);
529
530            _setParameters(form, input, stmt, dbType);
531            
532            stmt.executeUpdate();
533            
534            ConnectionHelper.cleanup(stmt);
535            
536            entryId = _getEntryId(connection, dbType, tableName);
537            
538            if (_formTableManager.hasWorkflowIdColumn(form.getId()))
539            {
540                success = _createWorkflow(form, objectModel, entryId);
541            }
542        }
543        catch (SQLException e)
544        {
545            getLogger().error("Error inserting submission data.", e);
546            success = false;
547        }
548        finally
549        {
550            ConnectionHelper.cleanup(stmt);
551            ConnectionHelper.cleanup(connection);
552        }
553        
554        return success ? entryId : null;
555    }
556
557    private String _getEntryId(Connection connection, String dbType, String tableName) throws SQLException, InvalidModificationException
558    {
559        String id = null;
560        if (ConnectionHelper.DATABASE_MYSQL.equals(dbType))
561        {
562            try (PreparedStatement stmt = connection.prepareStatement("SELECT id FROM " + _sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, tableName) + " WHERE id = last_insert_id()");
563                 ResultSet rs = stmt.executeQuery())
564            {
565                if (rs.next())
566                {
567                    id = rs.getString("id");
568                }
569                else
570                {
571                    if (connection.getAutoCommit())
572                    {
573                        throw new InvalidModificationException("Cannot retrieve inserted group. Group was created but listeners not called : base may be inconsistant");
574                    }
575                    else
576                    {
577                        connection.rollback();
578                        throw new InvalidModificationException("Cannot retrieve inserted group. Rolling back");
579                    }
580                }
581            }
582        }
583        else if (ConnectionHelper.DATABASE_DERBY.equals(dbType))
584        {
585            try (PreparedStatement stmt = connection.prepareStatement("VALUES IDENTITY_VAL_LOCAL ()"); 
586                 ResultSet rs = stmt.executeQuery())
587            {
588                if (rs.next())
589                {
590                    id = rs.getString(1);
591                }
592            }
593        }
594        else if (ConnectionHelper.DATABASE_HSQLDB.equals(dbType))
595        {
596            
597            try (PreparedStatement stmt = connection.prepareStatement("CALL IDENTITY ()");
598                 ResultSet rs = stmt.executeQuery())
599            {
600                if (rs.next())
601                {
602                    id = rs.getString(1);
603                }
604            }
605        }
606        else if (ConnectionHelper.DATABASE_POSTGRES.equals(dbType))
607        {
608            try (PreparedStatement stmt = connection.prepareStatement("SELECT currval('groups_id_seq')");
609                 ResultSet rs = stmt.executeQuery())
610            {
611                if (rs.next())
612                {
613                    id = rs.getString(1);
614                }
615            }
616        }
617        
618        return id;
619    }
620    
621    private boolean _createWorkflow(Form form, Map objectModel, String entryId) throws InvalidActionException
622    {
623        boolean success = true;
624        if (entryId != null)
625        {
626            // create workflow with entry id
627            Workflow workflow = _workflowProvider.getExternalWorkflow(JdbcWorkflowStore.ROLE);
628            
629            String workflowName = form.getWorkflowName();
630            int initialActionId = _workflowHelper.getInitialAction(workflowName); 
631            
632            Map<String, Object> inputs = new HashMap<>();
633            inputs.put("formId", form.getId());
634            inputs.put("entryId", entryId);
635            inputs.put(ObjectModelHelper.PARENT_CONTEXT, ObjectModelHelper.getContext(objectModel));
636            inputs.put(ObjectModelHelper.REQUEST_OBJECT, ObjectModelHelper.getRequest(objectModel));
637            
638            try
639            {
640                long workflowInstanceId = workflow.initialize(form.getWorkflowName(), initialActionId, inputs);
641                // insert workflow id in db
642                success = _updateWorkflowId(form.getId(), entryId, workflowInstanceId);
643            }
644            catch (Exception e) 
645            {
646                getLogger().error("Error inserting submission data.", e);
647                _removeFormEntry(form.getId(), entryId);
648                success = false;
649            }
650        }
651        
652        return success;
653    }
654
655    private boolean _removeFormEntry(String formId, String entryId)
656    {
657        boolean success = true;
658        
659        final String tableName = FormTableManager.TABLE_PREFIX + formId;
660
661        Connection connection = null;
662        PreparedStatement stmt = null;
663        
664        try
665        {
666            String dataSourceId = Config.getInstance().getValue(FormTableManager.FORMS_POOL_CONFIG_PARAM);
667            connection = ConnectionHelper.getConnection(dataSourceId);
668            
669            String dbType = ConnectionHelper.getDatabaseType(connection);
670
671            StringBuilder sql = new StringBuilder();
672
673            sql.append("DELETE FROM ").append(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, tableName))
674            .append(" WHERE id = ?");
675
676            stmt = connection.prepareStatement(sql.toString());
677            stmt.setString(1, entryId);
678            
679            stmt.executeUpdate();
680        }
681        catch (SQLException e)
682        {
683            getLogger().error("Error inserting submission data.", e);
684            success = false;
685        }
686        finally
687        {
688            ConnectionHelper.cleanup(stmt);
689            ConnectionHelper.cleanup(connection);
690        }
691        
692        return success;
693    }
694
695    private boolean _updateWorkflowId(String formId, String entryId, long workflowId)
696    {
697        boolean success = true;
698        
699        final String tableName = FormTableManager.TABLE_PREFIX + formId;
700
701        Connection connection = null;
702        PreparedStatement stmt = null;
703        
704        try
705        {
706            String dataSourceId = Config.getInstance().getValue(FormTableManager.FORMS_POOL_CONFIG_PARAM);
707            connection = ConnectionHelper.getConnection(dataSourceId);
708            
709            String dbType = ConnectionHelper.getDatabaseType(connection);
710
711            StringBuilder sql = new StringBuilder();
712
713            sql.append("UPDATE ").append(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, tableName))
714            .append(" SET ").append(_sqlDatabaseTypeExtensionPoint.languageEscapeTableName(dbType, FormTableManager.WORKFLOW_ID_FIELD)).append(" = ?")
715            .append(" WHERE id = ?");
716
717            stmt = connection.prepareStatement(sql.toString());
718            stmt.setLong(1, workflowId);
719            stmt.setString(2, entryId);
720            
721            stmt.executeUpdate();
722        }
723        catch (SQLException e)
724        {
725            getLogger().error("Error inserting submission data.", e);
726            success = false;
727        }
728        finally
729        {
730            ConnectionHelper.cleanup(stmt);
731            ConnectionHelper.cleanup(connection);
732        }
733        
734        return success;
735    }
736
737    /**
738     * Set the parameters into the prepared statement.
739     * 
740     * @param form the form.
741     * @param input the user input.
742     * @param stmt the prepared statement.
743     * @param dbType the database type.
744     * @throws SQLException if a SQL error occurs.
745     * @throws WorkflowException if an exception occurs while initializing a workflow instance for a form entry
746     */
747    protected void _setParameters(Form form, Map<String, FieldValue> input, PreparedStatement stmt, String dbType) throws SQLException, WorkflowException
748    {
749        // First two are id (auto-increment) and creation date
750        int index = 1;
751        if (DbTypeHelper.insertIdentity(dbType) && !ConnectionHelper.DATABASE_ORACLE.equals(dbType))
752        {
753            DbTypeHelper.setIdentity(stmt, index, dbType);
754            index++;
755        }
756
757        stmt.setTimestamp(index, new Timestamp(System.currentTimeMillis()));
758        index++;
759
760        UserIdentity user = _currentUserProvied.getUser();
761        if (user != null)
762        {
763            stmt.setString(index, user.getLogin());
764            index++;
765            
766            stmt.setString(index, user.getPopulationId());
767            index++;
768        }
769        else
770        {
771            stmt.setNull(index, Types.VARCHAR);
772            index++;
773            stmt.setNull(index, Types.VARCHAR);
774            index++;
775        }
776
777        // Then set all the entries to be stored into the prepared statement.
778        Collection<FieldValue> entries = _getEntriesToInsert(input.values());
779        for (FieldValue entry : entries)
780        {
781            if (entry.getField().getType() == FieldType.COST)
782            {
783                stmt.setString(index, _computeCost(input));
784            }
785            else if (entry.getValue() == null)
786            {
787                stmt.setNull(index, entry.getType());
788                if (entry.getType() == Types.BLOB)
789                {
790                    index++;
791                    stmt.setNull(index, Types.VARCHAR);
792                }
793            }
794            else if (entry.getType() == Types.BLOB && entry.getValue() instanceof File)
795            {
796                File file = (File) entry.getValue();
797
798                try
799                {
800                    _sqlDatabaseTypeExtensionPoint.setBlob(dbType, stmt, index, new FileInputStream(file), file.length());
801                    index++;
802
803                    stmt.setString(index, file.getName());
804                }
805                catch (IOException e)
806                {
807                    // Should never happen, as it was checked before.
808                    getLogger().error("Can't read uploaded file.", e);
809                }
810            }
811            else if (entry.getType() == Types.BOOLEAN && entry.getValue() instanceof Boolean)
812            {
813                stmt.setInt(index, Boolean.TRUE.equals(entry.getValue()) ? 1 : 0);
814            }
815            else
816            {
817                stmt.setObject(index, entry.getValue(), entry.getType());
818            }
819            index++;
820        }
821        
822        // Ensure compatibility with form entries that remained without workflow
823        if (_formTableManager.hasWorkflowIdColumn(form.getId()))
824        {
825            stmt.setLong(index, -1);
826        }
827    }
828
829    private String _computeCost(Map<String, FieldValue> input)
830    {
831        double v = input.values().stream()
832            .filter(fv -> fv.getField().getType() == FieldType.SELECT 
833                        && "true".equals(fv.getField().getProperties().getOrDefault("partofcost", "false")))
834            .mapToDouble(this::_getCost)
835            .sum();
836        
837        return Double.toString(v);
838    }
839    
840    private double _getCost(FieldValue fv)
841    {
842        return fv.getField().getProperties().entrySet().stream()
843            .mapToDouble(e -> {
844                Matcher m = __OPTION_INDEX.matcher(e.getKey());
845                if (m.matches() && _equals(e.getValue(), fv.getValue()))
846                {
847                    String v = fv.getField().getProperties().getOrDefault("option-" + m.group(1) + "-cost", "0");
848                    return Double.parseDouble(v);
849                }
850                else
851                {
852                    return 0.0;
853                }
854            })
855            .sum();
856    }
857    
858    private boolean _equals(String optionVaue, Object rawSelectedValues)
859    {
860        String[] selectedValues = StringUtils.split((String) rawSelectedValues, '\n');
861        for (String selectedValue : selectedValues)
862        {
863            if (StringUtils.equals(optionVaue.trim(), selectedValue.trim())) // Comparing trim value since some rendering (including default rendering) add many leading spaces
864            {
865                return true;
866            }
867        }
868        return false;
869    }
870
871    /**
872     * Validate the user input.
873     * 
874     * @param form the Form object.
875     * @param input the user input.
876     * @param errors the FormErrors object to fill.
877     * @param request the user request.
878     */
879    protected void _validateInput(Form form, Map<String, FieldValue> input, FormErrors errors, Request request)
880    {
881        for (FieldValue entry : input.values())
882        {
883            Field field = entry.getField();
884            switch (field.getType())
885            {
886                case TEXT:
887                    errors.addErrors(field.getId(), _validateTextField(entry, request));
888                    break;
889                case PASSWORD:
890                    errors.addErrors(field.getId(), _validatePassword(entry, request));
891                    break;
892                case SELECT:
893                    errors.addErrors(field.getId(), _validateSelect(entry, request));
894                    break;
895                case TEXTAREA:
896                    errors.addErrors(field.getId(), _validateTextarea(entry, request));
897                    break;
898                case RADIO:
899                    errors.addErrors(field.getId(), _validateRadio(entry, request));
900                    break;
901                case CHECKBOX:
902                    errors.addErrors(field.getId(), _validateCheckbox(entry, request));
903                    break;
904                case FILE:
905                    errors.addErrors(field.getId(), _validateFile(entry, request));
906                    break;
907                case CAPTCHA:
908                    errors.addErrors(field.getId(), _validateCaptcha(entry, request));
909                    break;
910                case HIDDEN:
911                default:
912                    break;
913            }
914        }
915    }
916    
917    /**
918     * Validate a text field.
919     * 
920     * @param entry the text field entry.
921     * @param request the user request.
922     * @return the list of error messages.
923     */
924    protected List<I18nizableText> _validateTextField(FieldValue entry, Request request)
925    {
926        List<I18nizableText> errors = new ArrayList<>();
927
928        Field field = entry.getField();
929        Map<String, String> properties = field.getProperties();
930        String value = StringUtils.defaultString((String) entry.getValue());
931
932        final String textPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_TEXT_";
933
934        errors.addAll(_validateMandatory(entry, textPrefix));
935
936        errors.addAll(_validateConfirmation(entry, request, textPrefix));
937
938        String regexpType = properties.get("regexptype");
939        if (StringUtils.isEmpty(regexpType) || "text".equals(regexpType))
940        {
941            errors.addAll(_validateTextLength(entry, textPrefix));
942        }
943        else if (!StringUtils.isBlank(value))
944        {
945            _validateNonblankRegexp(entry, errors, value, textPrefix, regexpType);
946        }
947
948        return errors;
949    }
950
951    private void _validateNonblankRegexp(FieldValue entry, List<I18nizableText> errors, String value, final String textPrefix, String regexpType)
952    {
953        if ("int".equals(regexpType) && StringUtils.isBlank(value))
954        {
955            errors.addAll(_validateInteger(entry, textPrefix));
956        }
957        else if ("float".equals(regexpType))
958        {
959            errors.addAll(_validateFloat(entry, textPrefix));
960        }
961        else if ("email".equals(regexpType))
962        {
963            if (StringUtils.isNotEmpty(value) && !_EMAIL_PATTERN.matcher(value).matches())
964            {
965                errors.add(new I18nizableText("plugin." + _pluginName, "PLUGINS_FORMS_FORMS_RENDER_ERROR_TEXT_EMAIL"));
966            }
967        }
968        else if ("phone".equals(regexpType))
969        {
970            if (StringUtils.isNotEmpty(value) && !_PHONE_PATTERN.matcher(value).matches())
971            {
972                errors.add(new I18nizableText("plugin." + _pluginName, "PLUGINS_FORMS_FORMS_RENDER_ERROR_TEXT_PHONE"));
973            }
974        }
975        else if ("date".equals(regexpType))
976        {
977            errors.addAll(_validateDate(entry, textPrefix));
978        }
979        else if ("time".equals(regexpType))
980        {
981            errors.addAll(_validateTime(entry, textPrefix));
982        }
983        else if ("datetime".equals(regexpType))
984        {
985            errors.addAll(_validateDateTime(entry, textPrefix));
986        }
987        else if ("custom".equals(regexpType))
988        {
989            errors.addAll(_validateCustomRegexp(entry, textPrefix));
990        }
991    }
992
993    /**
994     * Validate a password field.
995     * 
996     * @param entry the password field entry.
997     * @param request the user request.
998     * @return the list of error messages.
999     */
1000    protected List<I18nizableText> _validatePassword(FieldValue entry, Request request)
1001    {
1002        List<I18nizableText> errors = new ArrayList<>();
1003
1004        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_PASSWORD_";
1005
1006        errors.addAll(_validateMandatory(entry, keyPrefix));
1007
1008        errors.addAll(_validateConfirmation(entry, request, keyPrefix));
1009
1010        errors.addAll(_validateCustomRegexp(entry, keyPrefix));
1011
1012        return errors;
1013    }
1014
1015    /**
1016     * Validate a select input.
1017     * 
1018     * @param entry the select input entry.
1019     * @param request the user request.
1020     * @return the list of error messages.
1021     */
1022    protected List<I18nizableText> _validateSelect(FieldValue entry, Request request)
1023    {
1024        List<I18nizableText> errors = new ArrayList<>();
1025
1026        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_SELECT_";
1027
1028        errors.addAll(_validateMandatory(entry, keyPrefix));
1029
1030        return errors;
1031    }
1032
1033    /**
1034     * Validate a textarea.
1035     * 
1036     * @param entry the textarea entry.
1037     * @param request the user request.
1038     * @return the list of error messages.
1039     */
1040    protected List<I18nizableText> _validateTextarea(FieldValue entry, Request request)
1041    {
1042        List<I18nizableText> errors = new ArrayList<>();
1043
1044        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_TEXTAREA_";
1045
1046        errors.addAll(_validateMandatory(entry, keyPrefix));
1047
1048        return errors;
1049    }
1050
1051    /**
1052     * Validate a radio input.
1053     * 
1054     * @param entry the radio input entry.
1055     * @param request the user request.
1056     * @return the list of error messages.
1057     */
1058    protected List<I18nizableText> _validateRadio(FieldValue entry, Request request)
1059    {
1060        List<I18nizableText> errors = new ArrayList<>();
1061
1062        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_RADIO_";
1063
1064        errors.addAll(_validateMandatory(entry, keyPrefix));
1065
1066        return errors;
1067
1068    }
1069
1070    /**
1071     * Validate a checkbox input.
1072     * 
1073     * @param entry the checkbox entry.
1074     * @param request the user request.
1075     * @return the list of error messages.
1076     */
1077    protected List<I18nizableText> _validateCheckbox(FieldValue entry, Request request)
1078    {
1079        List<I18nizableText> errors = new ArrayList<>();
1080        Field field = entry.getField();
1081        Map<String, String> properties = field.getProperties();
1082        Boolean value = (Boolean) entry.getValue();
1083
1084        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_CHECKBOX_";
1085
1086        if (Boolean.parseBoolean(properties.get("mandatory")) && !value)
1087        {
1088            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "MANDATORY"));
1089        }
1090
1091        return errors;
1092    }
1093
1094    /**
1095     * Validate a file input.
1096     * 
1097     * @param entry the file input entry.
1098     * @param request the user request.
1099     * @return the list of error messages.
1100     */
1101    protected List<I18nizableText> _validateFile(FieldValue entry, Request request)
1102    {
1103        List<I18nizableText> errors = new ArrayList<>();
1104        
1105        Field field = entry.getField();
1106        Map<String, String> properties = field.getProperties();
1107        File file = (File) entry.getValue();
1108        
1109        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_FILE_";
1110        
1111        if (Boolean.parseBoolean(properties.get("mandatory")) && file == null)
1112        {
1113            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "MANDATORY"));
1114        }
1115        
1116        if (file != null)
1117        {
1118            // Validate file readability.
1119            if (!file.isFile() || !file.canRead())
1120            {
1121                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "INVALID"));
1122            }
1123            
1124            // Validate file extensions.
1125            String fileExtensions = StringUtils.defaultString(properties.get("fileextension"));
1126            String[] fileExtArray = fileExtensions.split(",");
1127            
1128            boolean extensionOk = false;
1129            for (int i = 0; i < fileExtArray.length && !extensionOk; i++)
1130            {
1131                String ext = fileExtArray[i].trim().toLowerCase();
1132                extensionOk = file.getName().toLowerCase().endsWith(ext);
1133            }
1134            
1135            if (!extensionOk)
1136            {
1137                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "EXTENSION"));
1138            }
1139            
1140            float maxLength = _getFloat(properties.get("maxsize"), Float.MAX_VALUE);
1141            if (maxLength < Float.MAX_VALUE)
1142            {
1143                maxLength = maxLength * 1024 * 1024;
1144            }
1145            
1146            if (file.length() > maxLength)
1147            {
1148                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "TOOLARGE", Collections.singletonList(properties.get("maxsize"))));
1149            }
1150            
1151            if (!_analyseFileForVirusHelper.analysefile(file.getAbsolutePath()))
1152            {
1153                errors.add(new I18nizableText("plugin.web", "PLUGINS_WEB_ERROR_FILE_INFECTED"));
1154            }
1155        }
1156        
1157        return errors;
1158    }
1159
1160    /**
1161     * Validate a captcha.
1162     * 
1163     * @param entry the captcha entry.
1164     * @param request the user request.
1165     * @return the list of error messages.
1166     */
1167    protected List<I18nizableText> _validateCaptcha(FieldValue entry, Request request)
1168    {
1169        List<I18nizableText> errors = new ArrayList<>();
1170
1171        final String keyPrefix = "PLUGINS_FORMS_FORMS_RENDER_ERROR_CAPTCHA_";
1172
1173        String[] values = (String[]) entry.getValue();
1174        String value = values[0];
1175        String key = values[1];
1176
1177        if (!CaptchaHelper.checkAndInvalidate(key, value))
1178        {
1179            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "INVALID"));
1180        }
1181        return errors;
1182    }
1183
1184    /**
1185     * Validate a mandatory field.
1186     * @param entry the field value
1187     * @param keyPrefix the key profix
1188     * @return the list of error messages.
1189     */
1190    protected List<I18nizableText> _validateMandatory(FieldValue entry, String keyPrefix)
1191    {
1192        List<I18nizableText> errors = new ArrayList<>();
1193
1194        Field field = entry.getField();
1195        Map<String, String> properties = field.getProperties();
1196        String value = StringUtils.defaultString((String) entry.getValue());
1197
1198        if (Boolean.parseBoolean(properties.get("mandatory")) && StringUtils.isBlank(value))
1199        {
1200            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "MANDATORY"));
1201        }
1202
1203        return errors;
1204    }
1205
1206    /**
1207     * Validate a confirmation.
1208     * @param entry the field value
1209     * @param request the request
1210     * @param keyPrefix the key prefix
1211     * @return the list of error messages.
1212     */
1213    protected List<I18nizableText> _validateConfirmation(FieldValue entry, Request request, String keyPrefix)
1214    {
1215        List<I18nizableText> errors = new ArrayList<>();
1216
1217        Field field = entry.getField();
1218        Map<String, String> properties = field.getProperties();
1219        String value = StringUtils.defaultString((String) entry.getValue());
1220
1221        if (Boolean.parseBoolean(properties.get("confirmation")))
1222        {
1223            String confName = field.getName() + "_confirmation";
1224            String confValue = request.getParameter(confName);
1225
1226            if (!value.equals(confValue))
1227            {
1228                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "CONFIRMATION"));
1229            }
1230        }
1231
1232        return errors;
1233    }
1234
1235    private List<I18nizableText> _validateTextLength(FieldValue entry, String keyPrefix)
1236    {
1237        List<I18nizableText> errors = new ArrayList<>();
1238
1239        Field field = entry.getField();
1240        Map<String, String> properties = field.getProperties();
1241        String value = StringUtils.defaultString((String) entry.getValue());
1242
1243        Integer minValue = _getInteger(properties.get("minvalue"), null);
1244
1245        if (minValue != null)
1246        {
1247            if (StringUtils.isNotBlank(value) && value.length() < minValue)
1248            {
1249                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "MINLENGTH", Collections.singletonList(minValue.toString())));
1250            }
1251        }
1252
1253        return errors;
1254    }
1255
1256    private List<I18nizableText> _validateInteger(FieldValue entry, String keyPrefix)
1257    {
1258        List<I18nizableText> errors = new ArrayList<>();
1259
1260        Field field = entry.getField();
1261        Map<String, String> properties = field.getProperties();
1262        String value = StringUtils.defaultString((String) entry.getValue());
1263
1264        if (!_INT_PATTERN.matcher(value).matches())
1265        {
1266            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "INTEGER"));
1267        }
1268        else
1269        {
1270            Integer intValue = _getInteger(value, null);
1271            Integer minValue = _getInteger(properties.get("minvalue"), null);
1272            Integer maxValue = _getInteger(properties.get("maxvalue"), null);
1273
1274            if (minValue != null && intValue != null && intValue < minValue)
1275            {
1276                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "INTEGER_MINVALUE", Collections.singletonList(minValue.toString())));
1277            }
1278            if (maxValue != null && intValue != null && intValue > maxValue)
1279            {
1280                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "INTEGER_MAXVALUE", Collections.singletonList(maxValue.toString())));
1281            }
1282        }
1283
1284        return errors;
1285    }
1286
1287    private List<I18nizableText> _validateFloat(FieldValue entry, String keyPrefix)
1288    {
1289        List<I18nizableText> errors = new ArrayList<>();
1290
1291        Field field = entry.getField();
1292        Map<String, String> properties = field.getProperties();
1293        String value = StringUtils.defaultString((String) entry.getValue());
1294
1295        if (!_FLOAT_PATTERN.matcher(value).matches())
1296        {
1297            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "FLOAT"));
1298        }
1299        else
1300        {
1301            Float floatValue = _getFloat(value, null);
1302            if (floatValue != null)
1303            {
1304                Integer minValue = _getInteger(properties.get("minvalue"), null);
1305                Integer maxValue = _getInteger(properties.get("maxvalue"), null);
1306
1307                if (minValue != null && floatValue < minValue)
1308                {
1309                    errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "FLOAT_MINVALUE", Collections.singletonList(minValue.toString())));
1310                }
1311                if (maxValue != null && floatValue > maxValue)
1312                {
1313                    errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "FLOAT_MAXVALUE", Collections.singletonList(maxValue.toString())));
1314                }
1315            }
1316        }
1317
1318        return errors;
1319    }
1320
1321    private List<I18nizableText> _validateDate(FieldValue entry, String keyPrefix)
1322    {
1323        List<I18nizableText> errors = new ArrayList<>();
1324
1325        Field field = entry.getField();
1326        Map<String, String> properties = field.getProperties();
1327        String value = StringUtils.defaultString((String) entry.getValue());
1328
1329        if (!_DATE_PATTERN.matcher(value).matches())
1330        {
1331            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "DATE"));
1332        }
1333
1334        Date date = _getDate(value, null, _DATE_FORMAT);
1335        if (date != null)
1336        {
1337            Date minDate = _getDate(properties.get("minvalue"), null, _DATE_FORMAT);
1338            Date maxDate = _getDate(properties.get("maxvalue"), null, _DATE_FORMAT);
1339
1340            if (minDate != null && date.before(minDate))
1341            {
1342                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "DATE_MINVALUE", Collections.singletonList(properties.get("minvalue"))));
1343            }
1344            if (maxDate != null && date.after(maxDate))
1345            {
1346                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "DATE_MAXVALUE", Collections.singletonList(properties.get("maxvalue"))));
1347            }
1348        }
1349
1350        return errors;
1351    }
1352
1353    private List<I18nizableText> _validateTime(FieldValue entry, String keyPrefix)
1354    {
1355        List<I18nizableText> errors = new ArrayList<>();
1356
1357        Field field = entry.getField();
1358        Map<String, String> properties = field.getProperties();
1359        String value = StringUtils.defaultString((String) entry.getValue());
1360
1361        if (!_TIME_PATTERN.matcher(value).matches())
1362        {
1363            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "TIME"));
1364        }
1365
1366        Date time = _getDate(value, null, _TIME_FORMAT);
1367        if (time != null)
1368        {
1369            Date minTime = _getDate(properties.get("minvalue"), null, _TIME_FORMAT);
1370            Date maxTime = _getDate(properties.get("maxvalue"), null, _TIME_FORMAT);
1371
1372            if (minTime != null && time.before(minTime))
1373            {
1374                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "TIME_MINVALUE", Collections.singletonList(properties.get("minvalue"))));
1375            }
1376            if (maxTime != null && time.after(maxTime))
1377            {
1378                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "TIME_MAXVALUE", Collections.singletonList(properties.get("maxvalue"))));
1379            }
1380        }
1381
1382        return errors;
1383    }
1384
1385    private List<I18nizableText> _validateDateTime(FieldValue entry, String keyPrefix)
1386    {
1387        List<I18nizableText> errors = new ArrayList<>();
1388
1389        Field field = entry.getField();
1390        Map<String, String> properties = field.getProperties();
1391        String value = StringUtils.defaultString((String) entry.getValue());
1392
1393        if (!_DATETIME_PATTERN.matcher(value).matches())
1394        {
1395            errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "DATETIME"));
1396        }
1397
1398        Date date = _getDate(value, null, _DATETIME_FORMAT);
1399        if (date != null)
1400        {
1401            Date minDate = _getDate(properties.get("minvalue"), null, _DATETIME_FORMAT);
1402            Date maxDate = _getDate(properties.get("maxvalue"), null, _DATETIME_FORMAT);
1403
1404            if (minDate != null && date.before(minDate))
1405            {
1406                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "DATETIME_MINVALUE", Collections.singletonList(properties.get("minvalue"))));
1407            }
1408            if (maxDate != null && date.after(maxDate))
1409            {
1410                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "DATETIME_MAXVALUE", Collections.singletonList(properties.get("maxvalue"))));
1411            }
1412        }
1413
1414        return errors;
1415    }
1416
1417    private List<I18nizableText> _validateCustomRegexp(FieldValue entry, String keyPrefix)
1418    {
1419        List<I18nizableText> errors = new ArrayList<>();
1420
1421        Field field = entry.getField();
1422        Map<String, String> properties = field.getProperties();
1423        String value = StringUtils.defaultString((String) entry.getValue());
1424
1425        if (StringUtils.isNotBlank(value))
1426        {
1427            Integer minValue = _getInteger(properties.get("minvalue"), null);
1428
1429            if (minValue != null && value.length() < minValue)
1430            {
1431                errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "MINLENGTH", Collections.singletonList(minValue.toString())));
1432            }
1433
1434            String jsPattern = properties.get("regexp");
1435            if (StringUtils.isNotBlank(jsPattern))
1436            {
1437                try
1438                {
1439                    String decodedJsPattern = URIUtils.decode(StringUtils.defaultString(jsPattern));
1440                    Pattern pattern = _getPattern(decodedJsPattern);
1441
1442                    if (!pattern.matcher(value).matches())
1443                    {
1444                        errors.add(new I18nizableText("plugin." + _pluginName, keyPrefix + "REGEXP"));
1445                    }
1446                }
1447                catch (PatternSyntaxException e)
1448                {
1449                    // Ignore, just don't validate.
1450                }
1451            }
1452        }
1453
1454        return errors;
1455    }
1456    
1457    /**
1458     * Parses a JS pattern (i.e. "/^[a-z]+$/i")
1459     * @param jsRegexp the JS regexp string
1460     * @return the compiled Pattern object.
1461     */
1462    private Pattern _getPattern(String jsRegexp)
1463    {
1464        Pattern pattern = null;
1465        String regex = "";
1466        int flags = 0;
1467
1468        int firstSlash = jsRegexp.indexOf('/');
1469        int lastSlash = jsRegexp.lastIndexOf('/');
1470
1471        if (firstSlash > -1 && lastSlash > firstSlash)
1472        {
1473            regex = jsRegexp.substring(firstSlash + 1, lastSlash);
1474            if ("i".equals(jsRegexp.substring(lastSlash + 1)))
1475            {
1476                flags = Pattern.CASE_INSENSITIVE;
1477            }
1478            pattern = Pattern.compile(regex, flags);
1479        }
1480
1481        return pattern;
1482    }
1483
1484    private Integer _getInteger(String stringValue, Integer defaultValue)
1485    {
1486        Integer value = defaultValue;
1487        if (StringUtils.isNotEmpty(stringValue))
1488        {
1489            try
1490            {
1491                value = Integer.parseInt(stringValue);
1492            }
1493            catch (NumberFormatException e)
1494            {
1495                // Ignore.
1496            }
1497        }
1498        return value;
1499    }
1500
1501    private Float _getFloat(String stringValue, Float defaultValue)
1502    {
1503        Float value = defaultValue;
1504        if (StringUtils.isNotEmpty(stringValue))
1505        {
1506            try
1507            {
1508                value = Float.parseFloat(stringValue);
1509            }
1510            catch (NumberFormatException e)
1511            {
1512                // Ignore.
1513            }
1514        }
1515        return value;
1516    }
1517
1518    private Date _getDate(String stringValue, Date defaultValue, DateFormat format)
1519    {
1520        Date value = defaultValue;
1521        if (StringUtils.isNotEmpty(stringValue))
1522        {
1523            try
1524            {
1525                value = format.parse(stringValue);
1526            }
1527            catch (ParseException e)
1528            {
1529                // Ignore.
1530            }
1531        }
1532        return value;
1533    }
1534
1535    /**
1536     * Retain only the entries that are to be inserted in the database.
1537     * 
1538     * @param entries the entries to filter.
1539     * @return the entries to insert in the database.
1540     */
1541    protected Collection<FieldValue> _getEntriesToInsert(Collection<FieldValue> entries)
1542    {
1543        List<FieldValue> filteredEntries = new ArrayList<>(entries.size());
1544
1545        for (FieldValue entry : entries)
1546        {
1547            if (!FieldType.CAPTCHA.equals(entry.getField().getType()))
1548            {
1549                filteredEntries.add(entry);
1550            }
1551        }
1552
1553        return filteredEntries;
1554    }
1555
1556    /**
1557     * Send the receipt and notification emails.
1558     * 
1559     * @param form the Form object.
1560     * @param input the user input.
1561     * @param site the site.
1562     * @param totalSubmissions total number of submissions. Can be null if the form don't have limits.
1563     */
1564    protected void _sendEmails(Form form, Map<String, FieldValue> input, Site site, Integer totalSubmissions)
1565    {
1566        Config config = Config.getInstance();
1567        String sender = config.getValue("smtp.mail.from");
1568
1569        if (site != null)
1570        {
1571            sender = site.getValue("site-mail-from");
1572        }
1573
1574        _sendNotificationEmails(form, input, sender);
1575
1576        _sendReceiptEmail(form, input, sender);
1577
1578        if (!StringUtils.isEmpty(form.getLimit()) && Integer.parseInt(form.getLimit()) == totalSubmissions + 1)
1579        {
1580            _sendLimitEmail(form, input, sender);
1581        }
1582    }
1583
1584    /**
1585     * Send the notification emails.
1586     * 
1587     * @param form the form.
1588     * @param input the user input.
1589     * @param sender the sender e-mail.
1590     */
1591    protected void _sendNotificationEmails(Form form, Map<String, FieldValue> input, String sender)
1592    {
1593        Set<String> emails = form.getNotificationEmails();
1594        try
1595        {
1596            String params = "?type=results&form-name=" + form.getLabel();
1597            String subject = _getMail("subject.txt" + params, form, input);
1598            String html = _getMail("results.html" + params, form, input);
1599            String text = _getMail("results.txt" + params, form, input);
1600            Collection<File> files = _getFiles(input);
1601
1602            for (String email : emails)
1603            {
1604                if (StringUtils.isNotEmpty(email))
1605                {
1606                    try
1607                    {
1608                        SendMailHelper.newMail()
1609                                      .withSubject(subject)
1610                                      .withHTMLBody(html)
1611                                      .withTextBody(text)
1612                                      .withAttachments(files)
1613                                      .withSender(sender)
1614                                      .withRecipient(email)
1615                                      .sendMail();
1616                    }
1617                    catch (MessagingException | IOException e)
1618                    {
1619                        getLogger().error("Error sending the notification mail to " + email, e);
1620                    }
1621                }
1622            }
1623        }
1624        catch (IOException e)
1625        {
1626            getLogger().error("Error creating the notification message.", e);
1627        }
1628    }
1629
1630    /**
1631     * Send the receipt email.
1632     * 
1633     * @param form the form.
1634     * @param input the user input.
1635     * @param sender the sender e-mail.
1636     */
1637    protected void _sendReceiptEmail(Form form, Map<String, FieldValue> input, String sender)
1638    {
1639        String email = "";
1640        try
1641        {
1642            String receiptFieldId = form.getReceiptFieldId();
1643            if (StringUtils.isNotEmpty(receiptFieldId))
1644            {
1645                FieldValue receiptEntry = input.get(receiptFieldId);
1646
1647                if (receiptEntry.getValue() != null)
1648                {
1649                    email = receiptEntry.getValue().toString();
1650
1651                    if (_EMAIL_PATTERN.matcher(email).matches())
1652                    {
1653                        String subject = URIUtils.decode(form.getReceiptFieldSubject());
1654                        String bodyTxt = URIUtils.decode(form.getReceiptFieldBody());
1655                        String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>");
1656                        
1657                        if (bodyTxt.contains(__FORM_ENTRY_PATTERN))
1658                        {
1659                            String entry2html = _getMail("entry.html", form, input);
1660                            String entry2text = _getMail("entry.txt", form, input);
1661                            
1662                            bodyTxt = StringUtils.replace(bodyTxt, __FORM_ENTRY_PATTERN, entry2text);
1663                            bodyHTML = StringUtils.replace(bodyHTML, __FORM_ENTRY_PATTERN, entry2html);
1664                        }
1665                        
1666                        String overrideSender = form.getReceiptFieldFromAddress();
1667
1668                        SendMailHelper.newMail()
1669                                      .withSubject(subject)
1670                                      .withHTMLBody(bodyHTML)
1671                                      .withTextBody(bodyTxt)
1672                                      .withSender(StringUtils.isEmpty(overrideSender) ? sender : overrideSender)
1673                                      .withRecipient(email)
1674                                      .sendMail();
1675                    }
1676                }
1677            }
1678        }
1679        catch (MessagingException | IOException e)
1680        {
1681            getLogger().error("Error sending the receipt mail to " + email, e);
1682        }
1683    }
1684
1685    /**
1686     * Send the limit email.
1687     * 
1688     * @param form the form.
1689     * @param input the user input.
1690     * @param sender the sender e-mail.
1691     */
1692    protected void _sendLimitEmail(Form form, Map<String, FieldValue> input, String sender)
1693    {
1694        Set<String> emails = form.getNotificationEmails();
1695        try
1696        {
1697            String params = "?type=limit&form-name=" + form.getLabel();
1698            String subject = _getMail("subject.txt" + params, form, input);
1699            String html = _getMail("limit.html" + params, form, input);
1700            String text = _getMail("limit.txt" + params, form, input);
1701            Collection<File> files = _getFiles(input);
1702
1703            for (String email : emails)
1704            {
1705                if (StringUtils.isNotEmpty(email))
1706                {
1707                    try
1708                    {
1709                        SendMailHelper.newMail()
1710                                      .withSubject(subject)
1711                                      .withHTMLBody(html)
1712                                      .withTextBody(text)
1713                                      .withAttachments(files)
1714                                      .withSender(sender)
1715                                      .withRecipient(email)
1716                                      .sendMail();
1717                    }
1718                    catch (MessagingException e)
1719                    {
1720                        getLogger().error("Error sending the limit mail to " + email, e);
1721                    }
1722                }
1723            }
1724        }
1725        catch (IOException e)
1726        {
1727            getLogger().error("Error creating the limit message.", e);
1728        }
1729    }
1730    /**
1731     * Get a mail pipeline's content.
1732     * 
1733     * @param resource the mail resource pipeline (i.e. "results.html" or
1734     *            "receipt.txt").
1735     * @param form the Form.
1736     * @param input the user input.
1737     * @return the mail content.
1738     * @throws IOException if an error occurs.
1739     */
1740    protected String _getMail(String resource, Form form, Map<String, FieldValue> input) throws IOException
1741    {
1742        Source src = null;
1743
1744        try
1745        {
1746            String uri = "cocoon:/mail/" + resource;
1747            Map<String, Object> parameters = new HashMap<>();
1748            parameters.put("form", form);
1749            parameters.put("input", input);
1750
1751            src = _sourceResolver.resolveURI(uri, null, parameters);
1752            Reader reader = new InputStreamReader(src.getInputStream(), "UTF-8");
1753            return IOUtils.toString(reader);
1754        }
1755        finally
1756        {
1757            _sourceResolver.release(src);
1758        }
1759    }
1760
1761    /**
1762     * Get the files of a user input.
1763     * 
1764     * @param input the user input.
1765     * @return the files submitted by the user.
1766     */
1767    protected Collection<File> _getFiles(Map<String, FieldValue> input)
1768    {
1769        List<File> files = new ArrayList<>();
1770
1771        for (FieldValue entry : input.values())
1772        {
1773            if (FieldType.FILE.equals(entry.getField().getType()))
1774            {
1775                File file = (File) entry.getValue();
1776                if (file != null)
1777                {
1778                    files.add(file);
1779                }
1780            }
1781        }
1782
1783        return files;
1784    }
1785    
1786    /**
1787     * Class representing form informations
1788     */
1789    public static class FormInformations
1790    {
1791        private FormErrors _formErrors;
1792        private String _redirection;
1793        
1794        /**
1795         * Construction for a form information
1796         */
1797        public FormInformations()
1798        {
1799            _formErrors = null;
1800            _redirection = null;
1801        }
1802        
1803        /**
1804         * The form errors
1805         * @return the form errors
1806         */
1807        public FormErrors getFormErrors()
1808        {
1809            return _formErrors;
1810        }
1811        
1812        /**
1813         * Set the form errors
1814         * @param formErrors the form errors
1815         */
1816        public void setFormErrors(FormErrors formErrors)
1817        {
1818            _formErrors = formErrors;
1819        }
1820        
1821        /**
1822         * Get the form redirection
1823         * @return the redirection
1824         */
1825        public String getRedirection()
1826        {
1827            return _redirection;
1828        }
1829        
1830        /**
1831         * Set the form redirection
1832         * @param redirection the redirection
1833         */
1834        public void setRedirection(String redirection)
1835        {
1836            _redirection = redirection;
1837        }
1838    }
1839
1840}