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 -> 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}