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