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}