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