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