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.time.ZonedDateTime;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023
024import org.apache.avalon.framework.parameters.Parameters;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.cocoon.environment.ObjectModelHelper;
028import org.apache.cocoon.environment.Redirector;
029import org.apache.cocoon.environment.Request;
030import org.apache.cocoon.environment.SourceResolver;
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.core.captcha.CaptchaHelper;
034import org.ametys.core.cocoon.ActionResultGenerator;
035import org.ametys.core.user.CurrentUserProvider;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.user.UserManager;
038import org.ametys.plugins.forms.dao.FormEntryDAO;
039import org.ametys.plugins.forms.dao.FormEntryDAO.Sort;
040import org.ametys.plugins.forms.dao.FormQuestionDAO.FormEntryValues;
041import org.ametys.plugins.forms.helper.FormMailHelper;
042import org.ametys.plugins.forms.helper.FormMailHelper.LimitationMailType;
043import org.ametys.plugins.forms.helper.FormWorkflowHelper;
044import org.ametys.plugins.forms.helper.LimitedEntriesHelper;
045import org.ametys.plugins.forms.helper.ScheduleOpeningHelper;
046import org.ametys.plugins.forms.helper.ScheduleOpeningHelper.FormStatus;
047import org.ametys.plugins.forms.question.types.RestrictiveQuestionType;
048import org.ametys.plugins.forms.repository.Form;
049import org.ametys.plugins.forms.repository.FormEntry;
050import org.ametys.plugins.forms.repository.FormQuestion;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.RepositoryConstants;
053import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
054import org.ametys.runtime.authentication.AccessDeniedException;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.model.View;
057import org.ametys.web.cache.PageHelper;
058import org.ametys.web.repository.page.ModifiableZoneItem;
059import org.ametys.web.repository.page.SitemapElement;
060import org.ametys.web.repository.page.ZoneItem;
061import org.ametys.web.repository.site.Site;
062
063import com.google.common.collect.ArrayListMultimap;
064import com.google.common.collect.Multimap;
065/**
066 * Process the user entries to the form.
067 */
068public class ProcessFormAction extends AbstractProcessFormAction
069{
070    /** The catpcha key */
071    public static final String CAPTCHA_KEY = "ametys-captcha";
072    
073    /** The form workflow helper */
074    protected FormWorkflowHelper _formWorkflowHelper;
075    /** The current user provider */
076    protected CurrentUserProvider _currentUserProvider;
077    /** The users manager */
078    protected UserManager _userManager;
079    /** the Handle Limited Entries Helper */
080    protected LimitedEntriesHelper _limitedEntriesHelper;
081    /** The form mail helper */
082    protected FormMailHelper _formMailHelper;
083    /** The schedule opening helper */
084    protected ScheduleOpeningHelper _scheduleOpeningHelper;
085    /** The page helper */
086    protected PageHelper _pageHelper;
087    /** The form entry DAO */
088    protected FormEntryDAO _formEntryDAO;
089    
090    @Override
091    public void service(ServiceManager serviceManager) throws ServiceException
092    {
093        super.service(serviceManager);
094        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
095        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
096        _formWorkflowHelper = (FormWorkflowHelper) serviceManager.lookup(FormWorkflowHelper.ROLE);
097        _limitedEntriesHelper = (LimitedEntriesHelper) serviceManager.lookup(LimitedEntriesHelper.ROLE);
098        _formMailHelper = (FormMailHelper) serviceManager.lookup(FormMailHelper.ROLE);
099        _scheduleOpeningHelper = (ScheduleOpeningHelper) serviceManager.lookup(ScheduleOpeningHelper.ROLE);
100        _pageHelper = (PageHelper) serviceManager.lookup(PageHelper.ROLE);
101        _formEntryDAO = (FormEntryDAO) serviceManager.lookup(FormEntryDAO.ROLE);
102    }
103    
104    @Override
105    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
106    { 
107        Request request = ObjectModelHelper.getRequest(objectModel);
108        Map<String, String> result = _processForm(request);
109        if (result == null)
110        {
111            return null;
112        }
113        request.setAttribute(ActionResultGenerator.MAP_REQUEST_ATTR, result);
114        return EMPTY_MAP;
115    }
116
117    @Override
118    protected List<FormQuestion> _getRuleFilteredQuestions(Request request, Form form, FormEntryValues entryValues, Optional<Long> currentStepId)
119    {
120        // Get only readable questions 
121        return _formQuestionDAO.getRuleFilteredQuestions(form, entryValues, currentStepId, false, true);
122    }
123    
124    /**
125     * Process form
126     * @param request the request
127     * @return the results
128     */
129    protected Map<String, String> _processForm(Request request)
130    {
131        Map<String, String> result = new HashMap<>();
132        
133        String formId = request.getParameter("formId");
134        if (StringUtils.isNotEmpty(formId))
135        {
136            // Retrieve the current workspace.
137            String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
138            try
139            {
140                // Force the workspace.
141                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
142            
143                // Get the form object.
144                Form form = (Form) _resolver.resolveById(formId);
145                if (!_rightManager.currentUserHasReadAccess(form))
146                {
147                    throw new AccessDeniedException("Can't answer to the form data without convenient right");
148                }
149                
150                UserIdentity user = _currentUserProvider.getUser();
151                String clientIp = _limitedEntriesHelper.getClientIp(request);
152                
153                Multimap<String, I18nizableText> formErrors = ArrayListMultimap.create();
154                boolean canUserSubmit = _limitedEntriesHelper.canUserSubmit(form, user, clientIp);
155                boolean isOpen = _scheduleOpeningHelper.getStatus(form) == FormStatus.OPEN;
156                boolean isConfigured = _formDAO.isFormConfigured(form);
157                if (canUserSubmit && isOpen && isConfigured)
158                {
159                    if (!_checkCaptcha(request, form, formErrors))
160                    {
161                        request.setAttribute("form", form);
162                        request.setAttribute("form-errors", formErrors);
163                        return null;
164                    }
165                    
166                    // Add the user entries into jcr.
167                    FormEntry entry = _entryDAO.createEntry(form);
168                    try
169                    {
170                        Optional<Long> currentStepId = form.hasWorkflow() ? Optional.of(RestrictiveQuestionType.INITIAL_WORKFLOW_ID) : Optional.empty();
171                        
172                        View entryView = View.of(entry.getModel());
173                        Map<String, Object> formInputValues = _foAmetysObjectCreationHelper.getFormValues(request, entryView, "", formErrors);
174                        _adaptFormValuesForChoiceList(form, formInputValues);
175                        View filteredEntryView = _getRuleFilteredEntryView(request, form, entryView, new FormEntryValues(formInputValues, null), currentStepId);
176                        formErrors.putAll(_foAmetysObjectCreationHelper.validateValues(formInputValues, filteredEntryView));
177                        for (FormQuestion question : form.getQuestions())
178                        {
179                            if (filteredEntryView.hasModelViewItem(question.getNameForForm()))
180                            {
181                                question.getType().validateEntryValues(question, formInputValues, formErrors, currentStepId, new HashMap<>());
182                            }
183                        }
184                        
185                        if (!formErrors.isEmpty())
186                        {
187                            // If there were errors in the input, store it as a request attribute and stop.
188                            request.setAttribute("form", form);
189                            request.setAttribute("form-errors", formErrors);
190                            return null;
191                        }
192                        
193                        entry.synchronizeValues(filteredEntryView, formInputValues);
194                        
195                        _handleComputedValues(form.getQuestions(), entry, false);
196                        entry.setUser(_currentUserProvider.getUser());
197                        entry.setIP(clientIp);
198                        entry.setSubmitDate(ZonedDateTime.now());
199                        entry.setActive(true);
200                        _setEntryId(entry);
201                        
202                        _formWorkflowHelper.initializeWorkflow(entry);
203                        
204                        form.saveChanges();
205                        
206                        // send mail
207                        _sendEmails(entry);
208                        
209                        if (form.isQueueEnabled())
210                        {
211                            int totalSubmissions = form.getActiveEntries().size();
212                            long rankInQueue = totalSubmissions - form.getMaxEntries().get();
213                            result.put("isInQueue", String.valueOf(rankInQueue > 0));
214                            if (rankInQueue > 0)
215                            {
216                                result.put("rankInQueue", String.valueOf(rankInQueue));
217                            }
218                        }
219                    }
220                    catch (Exception e)
221                    {
222                        request.setAttribute("form", form);
223                        request.setAttribute("form-errors", formErrors);
224                        getLogger().error("An error occured while storing entry", e);
225                        return null;
226                    }
227                }
228                else
229                {
230                    if (!canUserSubmit)
231                    {
232                        formErrors.put("entries-limit-reached", new I18nizableText("plugin.forms", "PLUGINS_FORMS_ENTRIES_LIMIT_REACHED_ERROR"));
233                        request.setAttribute("form", form);
234                        request.setAttribute("form-errors", formErrors);
235                    }
236                    
237                    if (!isOpen)
238                    {
239                        formErrors.put("scheduled-not-open", new I18nizableText("plugin.forms", "PLUGINS_FORMS_OPENING_SCHEDULE_PROCESS_ERROR"));
240                        request.setAttribute("form", form);
241                        request.setAttribute("form-errors", formErrors);
242                    }
243                    
244                    return null;
245                }
246            }
247            finally 
248            {
249                // Restore context
250                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
251            }
252        }
253        
254        return result;
255    }
256    
257    /**
258     * Check the captcha if needed
259     * @param request the request
260     * @param form the form
261     * @param formErrors the form errors
262     * @return <code>true</code> if the captcha is good
263     */
264    protected boolean _checkCaptcha(Request request, Form form, Multimap<String, I18nizableText> formErrors)
265    {
266        String zoneItemId = request.getParameter("ametys-zone-item-id");
267        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
268        
269        if (!_isFormOnZoneItem(form, zoneItemId))
270        {
271            throw new AccessDeniedException("The form '" + form.getId() + "' doesn't belong to the zone item '" + zoneItemId + "'");
272        }
273        
274        SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement();
275        Site site = form.getSite();
276        String captchaPolicy = site.getValue("display-captcha-policy");
277        
278        if (_pageHelper.isCaptchaRequired(sitemapElement))
279        {
280            String captchaValue = request.getParameter(CAPTCHA_KEY);
281            String captchaKey = request.getParameter(CAPTCHA_KEY + "-key");
282            if (!CaptchaHelper.checkAndInvalidate(captchaKey, captchaValue))
283            {
284                formErrors.put(CAPTCHA_KEY, new I18nizableText("plugin.forms", "PLUGINS_FORMS_ERROR_CAPTCHA_INVALID"));
285                return false;
286            }
287        }
288        else if (captchaPolicy == null || "restricted".equals(captchaPolicy)) 
289        {
290            if (!_rightManager.currentUserHasReadAccess(sitemapElement))
291            {
292                throw new AccessDeniedException("The user try to answer to form '" + form.getId() + "' which belong to an other zone item '" + zoneItemId + "'");
293            }
294        }
295        
296        return true;
297    }
298    
299    private boolean _isFormOnZoneItem(Form form, String zoneItemId)
300    {
301        AmetysObjectIterable<ModifiableZoneItem> zoneItems = _formDAO.getFormZoneItems(form.getId(), form.getSiteName());
302        
303        return zoneItems.stream()
304                    .filter(z -> z.getId().equals(zoneItemId))
305                    .findAny()
306                    .isPresent();
307    }
308    
309    /**
310     * Set entry id (auto-incremental id)
311     * @param entry the entry
312     */
313    protected void _setEntryId(FormEntry entry)
314    {
315        List<FormEntry> formEntries = _formEntryDAO.getFormEntries(entry.getForm(), false, List.of(new Sort(FormEntry.ATTRIBUTE_ID, "descending")));
316        Long entryId = formEntries.isEmpty() ? 1L : formEntries.get(0).getEntryId() + 1;
317        entry.setEntryId(entryId);
318    }
319    
320    /**
321     * Send the receipt and notification emails.
322     * @param entry the current entry
323     */
324    protected void _sendEmails(FormEntry entry)
325    {
326        Form form = entry.getForm();
327        
328        Optional<String[]> adminEmails = form.getAdminEmails();
329        Optional<String> otherAdminEmails = form.getOtherAdminEmails();
330        if (adminEmails.isPresent() || otherAdminEmails.isPresent())
331        {
332            String[] emailsAsArray = _formMailHelper.getAdminEmails(form, entry);
333            
334            _formMailHelper.sendEmailsForAdmin(form, entry, emailsAsArray);
335            
336            if (form.isEntriesLimited())
337            {
338                int totalSubmissions = form.getActiveEntries().size();
339                Long maxEntries = form.getMaxEntries().get();
340                if (maxEntries == totalSubmissions)
341                {
342                    _formMailHelper.sendLimitationReachedMailForAdmin(entry, emailsAsArray, LimitationMailType.LIMIT);
343                }
344                else if (form.isQueueEnabled() && form.getQueueSize().isPresent() && form.getQueueSize().get() + maxEntries == totalSubmissions)
345                {
346                    _formMailHelper.sendLimitationReachedMailForAdmin(entry, emailsAsArray, LimitationMailType.QUEUE);
347                }
348            }
349        }
350
351        _formMailHelper.sendReceiptEmail(form, entry);
352    }
353}