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