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