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.newsletter.subscribe;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.UUID;
024import java.util.concurrent.TimeUnit;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.activity.Initializable;
028import org.apache.avalon.framework.parameters.Parameters;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Redirector;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.environment.SourceResolver;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.commons.lang3.Strings;
037
038import org.ametys.core.cocoon.ActionResultGenerator;
039import org.ametys.core.user.CurrentUserProvider;
040import org.ametys.core.user.User;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.core.user.UserManager;
043import org.ametys.plugins.newsletter.category.Category;
044import org.ametys.plugins.newsletter.daos.Subscriber;
045import org.ametys.plugins.newsletter.daos.SubscribersDAO.UnsubscribeOrigin;
046import org.ametys.web.WebConstants;
047import org.ametys.web.repository.page.Page;
048import org.ametys.web.repository.page.ZoneItem;
049
050import com.google.common.cache.Cache;
051import com.google.common.cache.CacheBuilder;
052
053/**
054 * This action subscribes an email address to a newsletter
055 *
056 */
057public class SubscribeAndManageAction extends AbstractSubscribeAction implements Initializable
058{
059
060    /** The user manager */
061    protected UserManager _userManager;
062    /** The current user provider */
063    protected CurrentUserProvider _currentUserProvider;
064    
065    private Cache<String, String> _cache;
066    
067    @Override
068    public void service(ServiceManager smanager) throws ServiceException
069    {
070        super.service(smanager);
071        _userManager = (UserManager) smanager.lookup(UserManager.ROLE);
072        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
073    }
074
075    public void initialize() throws Exception
076    {
077        _cache = CacheBuilder.newBuilder().expireAfterWrite(60, TimeUnit.MINUTES).build();
078    }
079    @Override
080    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
081    {
082        Request request = ObjectModelHelper.getRequest(objectModel);
083        
084        Map<String, Object> result = new HashMap<>();
085        
086        Map<String, Object> formResult = new HashMap<>();
087        result.put("FormResult", formResult);
088        
089        // Add user to prefill the email address
090        _addUser(result, formResult);
091        
092        ZoneItem currentZoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
093        String[] categories = currentZoneItem.getServiceParameters().getValue("categories");
094        _addCategories(result, categories);
095
096        
097        String zoneItemId = request.getParameter("zoneitem-id");
098        // Handle zoneItemId id null for subscription coming from links
099        if (zoneItemId == null || currentZoneItem.getId().equals(zoneItemId))
100        {
101            // Compute current step
102            
103            // If we have a back value, we go back to step 1.
104            if (request.getParameter("back") != null)
105            {
106                _goToStep1(request, formResult);
107            }
108            else if (request.getParameter("form-token") != null)
109            {
110                _goToStep3(request, formResult, categories, currentZoneItem);
111            }
112            else if (request.getParameter("email") != null || request.getParameter("token") != null)
113            {
114                // (validate email/captcha or token and get the user subscriptions to display them as checkboxes)
115                _goToStep2(request, formResult, categories, currentZoneItem);
116            }
117            else
118            {
119                // Check if we need captcha, and fill the email if it had one
120                _goToStep1(request, formResult);
121            }
122        }
123        else
124        {
125            // Check if we need captcha, and fill the email if it had one
126            _goToStep1(request, formResult);
127        }
128        request.setAttribute(ActionResultGenerator.MAP_REQUEST_ATTR, result);
129        return EMPTY_MAP;
130    }
131
132    private Map<String, Object> _goToStep1(Request request, Map<String, Object> formResult)
133    {
134        // Check if we need captcha on this page
135        Page page = (Page) request.getAttribute(Page.class.getName());
136        formResult.put("has-captcha", _pageHelper.isCaptchaRequired(page));
137        
138        // In case the user already filled an email address, we want to get it back
139        String email = request.getParameter("email");
140        if (email != null)
141        {
142            formResult.put("email", request.getParameter("email"));
143        }
144        
145        formResult.put("step", "1");
146        
147        return formResult;
148    }
149    
150    private Map<String, Object> _goToStep2(Request request, Map<String, Object> formResult, String[] categories, ZoneItem currentZoneItem)
151    {
152        String email = null;
153        String siteName = currentZoneItem.getZone().getSitemapElement().getSiteName();
154        
155        String token = request.getParameter("token");
156        if (StringUtils.isNotEmpty(token))
157        {
158            // We come from email link
159            Subscriber subscriber = _subscribersDao.getSubscriberByToken(token);
160            if (subscriber == null || !siteName.equals(subscriber.getSiteName()))
161            {
162                formResult.put("msg", "invalid-token");
163                return _goToStep1(request, formResult);
164            }
165            email = subscriber.getEmail();
166        }
167        else
168        {
169            // We come from form submission
170            if (!_validCaptcha(request, currentZoneItem.getZone().getSitemapElement()))
171            {
172                formResult.put("msg", "invalid-captcha");
173                return _goToStep1(request, formResult);
174            }
175            formResult.put("is-from-form", "true");
176            email =  request.getParameter("email");
177        }
178
179        formResult.put("email", email);
180        formResult.put("siteName", siteName);
181
182        // Validate email
183        if (!_validEmail(email))
184        {
185            formResult.put("msg", "invalid-email");
186            return _goToStep1(request, formResult);
187        }
188
189        // Get currently subscribe categories
190        String finalEmail = email;
191        List<String> alreadySubscribeTo = Arrays.stream(categories)
192            .filter(categoryId -> _subscribersDao.getSubscriber(finalEmail, siteName, categoryId) != null)
193            .collect(Collectors.toList());
194        
195        formResult.put("alreadySubscribeTo", alreadySubscribeTo);
196        
197        // Generate an expirable token that will be used to retrieve the user in step 3
198        String formToken = UUID.randomUUID().toString();
199        _cache.put(formToken, email);
200        formResult.put("form-token", formToken);
201        formResult.put("step", "2");
202        return formResult;
203    }
204
205    private Map _goToStep3(Request request, Map<String, Object> formResult, String[] categories, ZoneItem currentZoneItem)
206    {
207        String siteName = currentZoneItem.getZone().getSitemapElement().getSiteName();
208        String formToken = request.getParameter("form-token");
209        String email = _cache.getIfPresent(formToken);
210        // Check if the token is still valid
211        if (!_validEmail(email))
212        {
213            formResult.put("msg", "token-expired");
214            return _goToStep1(request, formResult);
215        }
216        
217        List<Subscriber> newSubscribers = new ArrayList<>();
218        List<String> newSubscriberTokens = new ArrayList<>();
219        List<String> removeSubscriptions = new ArrayList<>();
220        
221        for (String categoryId : categories)
222        {
223            Subscriber subscriber = _subscribersDao.getSubscriber(email, siteName, categoryId);
224            boolean wantToSubscribe = Strings.CS.equals("true", request.getParameter("category_" + categoryId));
225            if (subscriber == null && wantToSubscribe)
226            {
227                Category category = _getCategory(categoryId);
228                if (category != null)
229                {
230                    subscriber = _createSubscritpion(email, siteName, categoryId);
231                    newSubscribers.add(subscriber);
232                    newSubscriberTokens.add(subscriber.getToken());
233                }
234            }
235            else if (subscriber != null && !wantToSubscribe)
236            {
237                removeSubscriptions.add(subscriber.getToken());
238            }
239
240        }
241        _subscribersDao.modifySubscriptions(newSubscribers, removeSubscriptions, UnsubscribeOrigin.SUBSCRIBER);
242        
243        getLogger().info("The user with email '" + email + "' subscribed to the newsletters '" + String.join(",", newSubscriberTokens)
244            + "' and unsubscribed to the newsletters '" + String.join(",", removeSubscriptions) + "'");
245        
246        formResult.put("step", "3");
247        return formResult;
248    }
249
250    private void _addUser(Map<String, Object> result, Map<String, Object> formResult)
251    {
252        UserIdentity userIdentity = _currentUserProvider.getUser();
253        if (userIdentity != null)
254        {
255            User user = _userManager.getUser(userIdentity);
256            
257            result.put("user", Map.of("email", user.getEmail()));
258            formResult.put("email", user.getEmail());
259        }
260    }
261
262    private void _addCategories(Map<String, Object> result, String[] categories)
263    {
264        
265        List<Map<String, Object>> categoryObjectMap = Arrays.stream(categories).map(categoryId ->
266        {
267            Category category = this._getCategory(categoryId);
268            Map<String, Object> categoryMap = new HashMap<>();
269            categoryMap.put("id", category.getId());
270            categoryMap.put("title", category.getTitle());
271            categoryMap.put("description", category.getDescription());
272            return categoryMap;
273        }).collect(Collectors.toList());
274        result.put("categories", categoryObjectMap);
275    }
276}