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