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