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