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.actions; 017 018import java.time.ZonedDateTime; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023 024import org.apache.avalon.framework.parameters.Parameters; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.cocoon.environment.ObjectModelHelper; 028import org.apache.cocoon.environment.Redirector; 029import org.apache.cocoon.environment.Request; 030import org.apache.cocoon.environment.SourceResolver; 031import org.apache.commons.lang.StringUtils; 032 033import org.ametys.core.captcha.CaptchaHelper; 034import org.ametys.core.cocoon.ActionResultGenerator; 035import org.ametys.core.user.CurrentUserProvider; 036import org.ametys.core.user.UserIdentity; 037import org.ametys.core.user.UserManager; 038import org.ametys.plugins.forms.dao.FormEntryDAO; 039import org.ametys.plugins.forms.dao.FormEntryDAO.Sort; 040import org.ametys.plugins.forms.dao.FormQuestionDAO.FormEntryValues; 041import org.ametys.plugins.forms.helper.FormMailHelper; 042import org.ametys.plugins.forms.helper.FormMailHelper.LimitationMailType; 043import org.ametys.plugins.forms.helper.FormWorkflowHelper; 044import org.ametys.plugins.forms.helper.LimitedEntriesHelper; 045import org.ametys.plugins.forms.helper.ScheduleOpeningHelper; 046import org.ametys.plugins.forms.helper.ScheduleOpeningHelper.FormStatus; 047import org.ametys.plugins.forms.question.types.RestrictiveQuestionType; 048import org.ametys.plugins.forms.repository.Form; 049import org.ametys.plugins.forms.repository.FormEntry; 050import org.ametys.plugins.forms.repository.FormQuestion; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.repository.RepositoryConstants; 053import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 054import org.ametys.runtime.authentication.AccessDeniedException; 055import org.ametys.runtime.i18n.I18nizableText; 056import org.ametys.runtime.model.View; 057import org.ametys.web.cache.PageHelper; 058import org.ametys.web.repository.page.ModifiableZoneItem; 059import org.ametys.web.repository.page.SitemapElement; 060import org.ametys.web.repository.page.ZoneItem; 061import org.ametys.web.repository.site.Site; 062 063import com.google.common.collect.ArrayListMultimap; 064import com.google.common.collect.Multimap; 065/** 066 * Process the user entries to the form. 067 */ 068public class ProcessFormAction extends AbstractProcessFormAction 069{ 070 /** The catpcha key */ 071 public static final String CAPTCHA_KEY = "ametys-captcha"; 072 073 /** The form workflow helper */ 074 protected FormWorkflowHelper _formWorkflowHelper; 075 /** The current user provider */ 076 protected CurrentUserProvider _currentUserProvider; 077 /** The users manager */ 078 protected UserManager _userManager; 079 /** the Handle Limited Entries Helper */ 080 protected LimitedEntriesHelper _limitedEntriesHelper; 081 /** The form mail helper */ 082 protected FormMailHelper _formMailHelper; 083 /** The schedule opening helper */ 084 protected ScheduleOpeningHelper _scheduleOpeningHelper; 085 /** The page helper */ 086 protected PageHelper _pageHelper; 087 /** The form entry DAO */ 088 protected FormEntryDAO _formEntryDAO; 089 090 @Override 091 public void service(ServiceManager serviceManager) throws ServiceException 092 { 093 super.service(serviceManager); 094 _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE); 095 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 096 _formWorkflowHelper = (FormWorkflowHelper) serviceManager.lookup(FormWorkflowHelper.ROLE); 097 _limitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE); 098 _formMailHelper = (FormMailHelper) serviceManager.lookup(FormMailHelper.ROLE); 099 _scheduleOpeningHelper = (ScheduleOpeningHelper) serviceManager.lookup(ScheduleOpeningHelper.ROLE); 100 _pageHelper = (PageHelper) serviceManager.lookup(PageHelper.ROLE); 101 _formEntryDAO = (FormEntryDAO) serviceManager.lookup(FormEntryDAO.ROLE); 102 } 103 104 @Override 105 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 106 { 107 Request request = ObjectModelHelper.getRequest(objectModel); 108 Map<String, String> result = _processForm(request); 109 if (result == null) 110 { 111 return null; 112 } 113 request.setAttribute(ActionResultGenerator.MAP_REQUEST_ATTR, result); 114 return EMPTY_MAP; 115 } 116 117 @Override 118 protected List<FormQuestion> _getRuleFilteredQuestions(Request request, Form form, FormEntryValues entryValues, Optional<Long> currentStepId) 119 { 120 // Get only readable questions 121 return _formQuestionDAO.getRuleFilteredQuestions(form, entryValues, currentStepId, false, true); 122 } 123 124 /** 125 * Process form 126 * @param request the request 127 * @return the results 128 */ 129 protected Map<String, String> _processForm(Request request) 130 { 131 Map<String, String> result = new HashMap<>(); 132 133 String formId = request.getParameter("formId"); 134 if (StringUtils.isNotEmpty(formId)) 135 { 136 // Retrieve the current workspace. 137 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 138 try 139 { 140 // Force the workspace. 141 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE); 142 143 // Get the form object. 144 Form form = (Form) _resolver.resolveById(formId); 145 if (!_rightManager.currentUserHasReadAccess(form)) 146 { 147 throw new AccessDeniedException("Can't answer to the form data without convenient right"); 148 } 149 150 UserIdentity user = _currentUserProvider.getUser(); 151 String clientIp = _limitedEntriesHelper.getClientIp(request); 152 153 Multimap<String, I18nizableText> formErrors = ArrayListMultimap.create(); 154 boolean canUserSubmit = _limitedEntriesHelper.canUserSubmit(form, user, clientIp); 155 boolean isOpen = _scheduleOpeningHelper.getStatus(form) == FormStatus.OPEN; 156 boolean isConfigured = _formDAO.isFormConfigured(form); 157 if (canUserSubmit && isOpen && isConfigured) 158 { 159 if (!_checkCaptcha(request, form, formErrors)) 160 { 161 request.setAttribute("form", form); 162 request.setAttribute("form-errors", formErrors); 163 return null; 164 } 165 166 // Add the user entries into jcr. 167 FormEntry entry = _entryDAO.createEntry(form); 168 try 169 { 170 Optional<Long> currentStepId = form.hasWorkflow() ? Optional.of(RestrictiveQuestionType.INITIAL_WORKFLOW_ID) : Optional.empty(); 171 172 View entryView = View.of(entry.getModel()); 173 Map<String, Object> formInputValues = _foAmetysObjectCreationHelper.getFormValues(request, entryView, "", formErrors); 174 _adaptFormValuesForChoiceList(form, formInputValues); 175 View filteredEntryView = _getRuleFilteredEntryView(request, form, entryView, new FormEntryValues(formInputValues, null), currentStepId); 176 formErrors.putAll(_foAmetysObjectCreationHelper.validateValues(formInputValues, filteredEntryView)); 177 for (FormQuestion question : form.getQuestions()) 178 { 179 if (filteredEntryView.hasModelViewItem(question.getNameForForm())) 180 { 181 question.getType().validateEntryValues(question, formInputValues, formErrors, currentStepId, new HashMap<>()); 182 } 183 } 184 185 if (!formErrors.isEmpty()) 186 { 187 // If there were errors in the input, store it as a request attribute and stop. 188 request.setAttribute("form", form); 189 request.setAttribute("form-errors", formErrors); 190 return null; 191 } 192 193 entry.synchronizeValues(filteredEntryView, formInputValues); 194 195 _handleComputedValues(form.getQuestions(), entry, false); 196 entry.setUser(_currentUserProvider.getUser()); 197 entry.setIP(clientIp); 198 entry.setSubmitDate(ZonedDateTime.now()); 199 entry.setActive(true); 200 _setEntryId(entry); 201 202 _formWorkflowHelper.initializeWorkflow(entry); 203 204 form.saveChanges(); 205 206 // send mail 207 _sendEmails(entry); 208 209 if (form.isQueueEnabled()) 210 { 211 int totalSubmissions = form.getActiveEntries().size(); 212 long rankInQueue = totalSubmissions - form.getMaxEntries().get(); 213 result.put("isInQueue", String.valueOf(rankInQueue > 0)); 214 if (rankInQueue > 0) 215 { 216 result.put("rankInQueue", String.valueOf(rankInQueue)); 217 } 218 } 219 } 220 catch (Exception e) 221 { 222 request.setAttribute("form", form); 223 request.setAttribute("form-errors", formErrors); 224 getLogger().error("An error occured while storing entry", e); 225 return null; 226 } 227 } 228 else 229 { 230 if (!canUserSubmit) 231 { 232 formErrors.put("entries-limit-reached", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRIES_LIMIT_REACHED_ERROR")); 233 request.setAttribute("form", form); 234 request.setAttribute("form-errors", formErrors); 235 } 236 237 if (!isOpen) 238 { 239 formErrors.put("scheduled-not-open", new I18nizableText("plugin.forms", "PLUGINS_FORMS_OPENING_SCHEDULE_PROCESS_ERROR")); 240 request.setAttribute("form", form); 241 request.setAttribute("form-errors", formErrors); 242 } 243 244 return null; 245 } 246 } 247 finally 248 { 249 // Restore context 250 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 251 } 252 } 253 254 return result; 255 } 256 257 /** 258 * Check the captcha if needed 259 * @param request the request 260 * @param form the form 261 * @param formErrors the form errors 262 * @return <code>true</code> if the captcha is good 263 */ 264 protected boolean _checkCaptcha(Request request, Form form, Multimap<String, I18nizableText> formErrors) 265 { 266 String zoneItemId = request.getParameter("ametys-zone-item-id"); 267 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 268 269 if (!_isFormOnZoneItem(form, zoneItemId)) 270 { 271 throw new AccessDeniedException("The form '" + form.getId() + "' doesn't belong to the zone item '" + zoneItemId + "'"); 272 } 273 274 SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement(); 275 Site site = form.getSite(); 276 String captchaPolicy = site.getValue("display-captcha-policy"); 277 278 if (_pageHelper.isCaptchaRequired(sitemapElement)) 279 { 280 String captchaValue = request.getParameter(CAPTCHA_KEY); 281 String captchaKey = request.getParameter(CAPTCHA_KEY + "-key"); 282 if (!CaptchaHelper.checkAndInvalidate(captchaKey, captchaValue)) 283 { 284 formErrors.put(CAPTCHA_KEY, new I18nizableText("plugin.forms", "PLUGINS_FORMS_ERROR_CAPTCHA_INVALID")); 285 return false; 286 } 287 } 288 else if (captchaPolicy == null || "restricted".equals(captchaPolicy)) 289 { 290 if (!_rightManager.currentUserHasReadAccess(sitemapElement)) 291 { 292 throw new AccessDeniedException("The user try to answer to form '" + form.getId() + "' which belong to an other zone item '" + zoneItemId + "'"); 293 } 294 } 295 296 return true; 297 } 298 299 private boolean _isFormOnZoneItem(Form form, String zoneItemId) 300 { 301 AmetysObjectIterable<ModifiableZoneItem> zoneItems = _formDAO.getFormZoneItems(form.getId(), form.getSiteName()); 302 303 return zoneItems.stream() 304 .filter(z -> z.getId().equals(zoneItemId)) 305 .findAny() 306 .isPresent(); 307 } 308 309 /** 310 * Set entry id (auto-incremental id) 311 * @param entry the entry 312 */ 313 protected void _setEntryId(FormEntry entry) 314 { 315 List<FormEntry> formEntries = _formEntryDAO.getFormEntries(entry.getForm(), false, List.of(new Sort(FormEntry.ATTRIBUTE_ID, "descending"))); 316 Long entryId = formEntries.isEmpty() ? 1L : formEntries.get(0).getEntryId() + 1; 317 entry.setEntryId(entryId); 318 } 319 320 /** 321 * Send the receipt and notification emails. 322 * @param entry the current entry 323 */ 324 protected void _sendEmails(FormEntry entry) 325 { 326 Form form = entry.getForm(); 327 328 Optional<String[]> adminEmails = form.getAdminEmails(); 329 Optional<String> otherAdminEmails = form.getOtherAdminEmails(); 330 if (adminEmails.isPresent() || otherAdminEmails.isPresent()) 331 { 332 String[] emailsAsArray = _formMailHelper.getAdminEmails(form, entry); 333 334 _formMailHelper.sendEmailsForAdmin(form, entry, emailsAsArray); 335 336 if (form.isEntriesLimited()) 337 { 338 int totalSubmissions = form.getActiveEntries().size(); 339 Long maxEntries = form.getMaxEntries().get(); 340 if (maxEntries == totalSubmissions) 341 { 342 _formMailHelper.sendLimitationReachedMailForAdmin(entry, emailsAsArray, LimitationMailType.LIMIT); 343 } 344 else if (form.isQueueEnabled() && form.getQueueSize().isPresent() && form.getQueueSize().get() + maxEntries == totalSubmissions) 345 { 346 _formMailHelper.sendLimitationReachedMailForAdmin(entry, emailsAsArray, LimitationMailType.QUEUE); 347 } 348 } 349 } 350 351 _formMailHelper.sendReceiptEmail(form, entry); 352 } 353}