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