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