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