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