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}