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