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