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