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