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}