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