001/* 002 * Copyright 2022 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.helper; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Optional; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.service.Serviceable; 030import org.apache.cocoon.environment.Request; 031import org.apache.commons.lang.StringUtils; 032 033import org.ametys.core.observation.Event; 034import org.ametys.core.observation.ObservationManager; 035import org.ametys.core.ui.Callable; 036import org.ametys.core.user.CurrentUserProvider; 037import org.ametys.core.user.UserIdentity; 038import org.ametys.plugins.forms.FormEvents; 039import org.ametys.plugins.forms.dao.FormDAO; 040import org.ametys.plugins.forms.dao.FormEntryDAO; 041import org.ametys.plugins.forms.repository.Form; 042import org.ametys.plugins.forms.repository.FormEntry; 043import org.ametys.plugins.repository.AmetysObjectResolver; 044import org.ametys.plugins.repository.query.expression.Expression; 045import org.ametys.plugins.repository.query.expression.Expression.Operator; 046import org.ametys.plugins.repository.query.expression.StringExpression; 047import org.ametys.plugins.repository.query.expression.UserExpression; 048import org.ametys.runtime.config.Config; 049import org.ametys.runtime.plugin.component.AbstractLogEnabled; 050 051/** 052 * The helper to handler limited entries 053 */ 054public class LimitedEntriesHelper extends AbstractLogEnabled implements Serviceable, Component 055{ 056 /** Avalon ROLE. */ 057 public static final String ROLE = LimitedEntriesHelper.class.getName(); 058 059 /** Ametys object resolver. */ 060 protected AmetysObjectResolver _resolver; 061 062 /** The form mail helper */ 063 protected FormMailHelper _formMailHelper; 064 065 /** The form DAO */ 066 protected FormDAO _formDAO; 067 068 /** The current user provider */ 069 protected CurrentUserProvider _currentUserProvider; 070 071 /** Observer manager. */ 072 protected ObservationManager _observationManager; 073 074 /** The form entry DAO */ 075 protected FormEntryDAO _formEntryDAO; 076 077 public void service(ServiceManager manager) throws ServiceException 078 { 079 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 080 _formMailHelper = (FormMailHelper) manager.lookup(FormMailHelper.ROLE); 081 _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE); 082 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 083 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 084 _formEntryDAO = (FormEntryDAO) manager.lookup(FormEntryDAO.ROLE); 085 } 086 087 /** 088 * Get the form properties relevant for limiting entries number 089 * @param formId Id of the current form 090 * @return a map of the form properties 091 */ 092 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 093 public Map<String, Object> getLimitedEntriesProperties(String formId) 094 { 095 Map<String, Object> limitProperties = new HashMap<> (); 096 097 Form form = _resolver.resolveById(formId); 098 _formDAO.checkHandleFormRight(form); 099 100 limitProperties.put(Form.LIMIT_TO_ONE_ENTRY_BY_USER, form.isLimitedToOneEntryByUser()); 101 limitProperties.put(Form.LIMIT_ENTRIES_ENABLED, form.isEntriesLimited()); 102 _addOptionalProperty(Form.MAX_ENTRIES, form.getMaxEntries(), limitProperties, null); 103 _addOptionalProperty(Form.REMAINING_MSG, form.getRemainingMessage(), limitProperties, null); 104 _addOptionalProperty(Form.CLOSED_MSG, form.getClosedMessage(), limitProperties, null); 105 106 limitProperties.put(Form.QUEUE_ENABLED, form.isQueueEnabled()); 107 _addOptionalProperty(Form.QUEUE_SIZE, form.getQueueSize(), limitProperties, null); 108 _addOptionalProperty(Form.QUEUE_CLOSED_MSG, form.getClosedQueueMessage(), limitProperties, null); 109 _addOptionalProperty(Form.QUEUE_SENDER, form.getQueueMailSender(), limitProperties, Config.getInstance().getValue("smtp.mail.from")); 110 _addOptionalProperty(Form.QUEUE_RECEIVER, form.getQueueMailReceiver(), limitProperties, FormMailHelper.RECEIVER_COMBOBOX_ENTRY_USER_VALUE); 111 _addOptionalProperty(Form.QUEUE_SUBJECT, form.getQueueMailSubject(), limitProperties, null); 112 _addOptionalProperty(Form.QUEUE_BODY, form.getQueueMailBody(), limitProperties, null); 113 114 return limitProperties; 115 } 116 117 private void _addOptionalProperty(String propertyName, Optional<? extends Object> value, Map<String, Object> limitProperties, Object defaultValue) 118 { 119 if (value.isPresent()) 120 { 121 limitProperties.put(propertyName, value.get()); 122 } 123 else if (defaultValue != null) 124 { 125 limitProperties.put(propertyName, defaultValue); 126 } 127 } 128 129 /** 130 * Set the form properties relevant for limiting entries number 131 * @param limitParameters the limit parameters 132 * @return the map of results 133 */ 134 @Callable (rights = Callable.SKIP_BUILTIN_CHECK) 135 public Map<String, Object> setEntriesLimitProperties (Map<String, Object> limitParameters) 136 { 137 Map<String, Object> result = new HashMap<>(); 138 139 Form form = _resolver.resolveById((String) limitParameters.get("formId")); 140 _formDAO.checkHandleFormRight(form); 141 142 form.limitToOneEntryByUser((boolean) limitParameters.get(Form.LIMIT_TO_ONE_ENTRY_BY_USER)); 143 if ((boolean) limitParameters.get(Form.LIMIT_ENTRIES_ENABLED)) 144 { 145 form.limitEntries(true); 146 form.setMaxEntries(((Integer) limitParameters.get(Form.MAX_ENTRIES)).longValue()); 147 form.setRemainingMessage((String) limitParameters.get(Form.REMAINING_MSG)); 148 form.setClosedMessage((String) limitParameters.get(Form.CLOSED_MSG)); 149 150 if ((boolean) limitParameters.get(Form.QUEUE_ENABLED)) 151 { 152 form.enableQueue(true); 153 Object queueSize = limitParameters.get(Form.QUEUE_SIZE); 154 if (queueSize != null) 155 { 156 form.setQueueSize(((Integer) queueSize).longValue()); 157 } 158 else 159 { 160 form.removeValue(Form.QUEUE_SIZE); 161 } 162 form.setClosedQueueMessage((String) limitParameters.get(Form.QUEUE_CLOSED_MSG)); 163 form.setQueueMailtReceiver((String) limitParameters.get(Form.QUEUE_RECEIVER)); 164 form.setQueueMailSender((String) limitParameters.get(Form.QUEUE_SENDER)); 165 form.setQueueMailSubject((String) limitParameters.get(Form.QUEUE_SUBJECT)); 166 form.setQueueMailBody((String) limitParameters.get(Form.QUEUE_BODY)); 167 } 168 else 169 { 170 form.enableQueue(false); 171 } 172 } 173 else 174 { 175 form.limitEntries(false); 176 } 177 form.saveChanges(); 178 179 Map<String, Object> eventParams = new HashMap<>(); 180 eventParams.put("form", form); 181 _observationManager.notify(new Event(FormEvents.FORM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 182 183 return result; 184 } 185 186 /** 187 * <code>true</code> if the user can submit the form depending of the form limitation 188 * @param form the form 189 * @param user the user. Can be null if the form is anonymous 190 * @param clientIp the client ip of the user 191 * @return <code>true</code> if the user can submit the form depending of the form limitation 192 */ 193 public boolean canUserSubmit(Form form, UserIdentity user, String clientIp) 194 { 195 // Check if the form is limited to one entry by user and if the user has already answer 196 if (form.isLimitedToOneEntryByUser() && hasUserAlreadyAnswer(form, user, clientIp)) 197 { 198 return false; 199 } 200 201 // Check if the number of entries is limited 202 Optional<Long> maxEntriesOpt = form.getMaxEntries(); 203 if (form.isEntriesLimited() && maxEntriesOpt.isPresent()) 204 { 205 Optional<Long> queueSize = form.getQueueSize(); 206 int entriesSize = form.getActiveEntries().size(); 207 // If there are a queue ... 208 if (form.isQueueEnabled()) 209 { 210 // ... return true if the queue has no limit 211 // or check the entries size 212 return queueSize.isEmpty() || entriesSize < (maxEntriesOpt.get() + queueSize.get()); 213 } 214 else 215 { 216 // Just check the entries size with the limit of entries 217 return entriesSize < maxEntriesOpt.get(); 218 } 219 } 220 221 // Return true if no limitation 222 return true; 223 } 224 225 /** 226 * <code>true</code> if the user has already answer to the form 227 * @param form the form 228 * @param user the user. Can be null if the form is anonymous 229 * @param clientIp the client ip of the user 230 * @return <code>true</code> if the user has already answer to the form 231 */ 232 public boolean hasUserAlreadyAnswer(Form form, UserIdentity user, String clientIp) 233 { 234 Expression additionalQuery = user != null 235 ? new UserExpression(FormEntry.ATTRIBUTE_USER, Operator.EQ, user) 236 : StringUtils.isNotBlank(clientIp) 237 ? new StringExpression(FormEntry.ATTRIBUTE_IP, Operator.EQ, clientIp) 238 : null; 239 240 return !_formEntryDAO.getFormEntries(form, false, additionalQuery, List.of()).isEmpty(); 241 } 242 243 /** 244 * Get a forwarded client IP address if available. 245 * @param request The servlet request object. 246 * @return The HTTP <code>X-Forwarded-For</code> header value if present, 247 * or the default remote address if not. 248 */ 249 public String getClientIp(Request request) 250 { 251 String ip = ""; 252 String forwardedIpHeader = request.getHeader("X-Forwarded-For"); 253 254 if (StringUtils.isNotEmpty(forwardedIpHeader)) 255 { 256 String[] forwardedIps = forwardedIpHeader.split("[\\s,]+"); 257 String forwardedIp = forwardedIps[forwardedIps.length - 1]; 258 if (StringUtils.isNotBlank(forwardedIp)) 259 { 260 ip = forwardedIp.trim(); 261 } 262 } 263 264 if (StringUtils.isBlank(ip)) 265 { 266 ip = request.getRemoteAddr(); 267 } 268 return ip; 269 } 270 271 /** 272 * <code>true</code> if the entry is in queue 273 * @param entry the entry 274 * @return <code>true</code> if the entry is in queue 275 */ 276 public boolean isInQueue(FormEntry entry) 277 { 278 Form form = entry.getForm(); 279 return form.isQueueEnabled() && _getQueue(form).contains(entry); 280 } 281 282 /** 283 * Deactivate an entry handling the form limitation 284 * @param entryId the entry id 285 */ 286 public void deactivateEntry(String entryId) 287 { 288 FormEntry entryToDeactivate = _resolver.resolveById(entryId); 289 if (entryToDeactivate.isActive()) 290 { 291 Form form = entryToDeactivate.getForm(); 292 if (form.isQueueEnabled()) 293 { 294 List<FormEntry> queue = _getQueue(form); 295 // The entry is on the main list 296 if (!queue.isEmpty() && !queue.contains(entryToDeactivate)) 297 { 298 _formMailHelper.sendOutOfQueueMail(form, queue.get(0)); 299 } 300 } 301 302 // Deactivate the entry after calculating queue 303 entryToDeactivate.setActive(false); 304 } 305 } 306 307 /** 308 * Get the waiting list sort by the submiting date 309 * @param form the form 310 * @return the waiting list 311 */ 312 protected List<FormEntry> _getQueue(Form form) 313 { 314 List<FormEntry> queuedEntries = new ArrayList<>(); 315 316 Optional<Long> maxEntries = form.getMaxEntries(); 317 List<FormEntry> activeEntries = form.getActiveEntries(); 318 Collections.sort(activeEntries, Comparator.comparing(e -> e.getSubmitDate())); 319 320 for (int i = maxEntries.get().intValue(); i < activeEntries.size(); i++) 321 { 322 queuedEntries.add(activeEntries.get(i)); 323 } 324 325 return queuedEntries; 326 } 327 328 /** 329 * <code>true</code> if the form limit is reach 330 * @param form the form 331 * @return <code>true</code> if the form limit is reach 332 */ 333 public boolean isFormLimitIsReached(Form form) 334 { 335 Optional<Long> maxEntries = form.getMaxEntries(); 336 if (form.isEntriesLimited() && maxEntries.isPresent()) 337 { 338 List<FormEntry> activeEntries = form.getActiveEntries(); 339 return activeEntries.size() >= maxEntries.get(); 340 } 341 342 return false; 343 } 344}