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}