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}