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