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