001/* 002 * Copyright 2011 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.io.File; 019import java.io.FileInputStream; 020import java.io.IOException; 021import java.nio.charset.StandardCharsets; 022import java.util.Collection; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.LinkedHashSet; 026import java.util.Map; 027import java.util.Set; 028import java.util.UUID; 029import java.util.regex.Pattern; 030 031import org.apache.avalon.framework.parameters.Parameters; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.cocoon.acting.ServiceableAction; 035import org.apache.cocoon.environment.ObjectModelHelper; 036import org.apache.cocoon.environment.Redirector; 037import org.apache.cocoon.environment.Request; 038import org.apache.cocoon.environment.SourceResolver; 039import org.apache.cocoon.servlet.multipart.Part; 040import org.apache.cocoon.servlet.multipart.PartOnDisk; 041import org.apache.cocoon.servlet.multipart.RejectedPart; 042import org.apache.commons.io.FilenameUtils; 043import org.apache.commons.io.IOUtils; 044import org.apache.commons.io.input.BOMInputStream; 045import org.apache.commons.lang.StringUtils; 046 047import org.ametys.core.cocoon.JSonReader; 048import org.ametys.core.util.mail.SendMailHelper; 049import org.ametys.plugins.newsletter.category.Category; 050import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint; 051import org.ametys.plugins.newsletter.daos.Subscriber; 052import org.ametys.plugins.newsletter.daos.SubscribersDAO; 053 054/** 055 * Import subscribers from a CSV or text file. 056 */ 057public class ImportSubscribersAction extends ServiceableAction 058{ 059 private static final String[] _ALLOWED_EXTENSIONS = new String[] {"txt", "csv"}; 060 061 private static final Pattern __EMAIL_VALIDATOR = SendMailHelper.EMAIL_VALIDATION; 062 063 /** The subscribers DAO. */ 064 protected SubscribersDAO _subscribersDao; 065 066 /** The category provider extension point. */ 067 protected CategoryProviderExtensionPoint _categoryProviderEP; 068 069 @Override 070 public void service(ServiceManager smanager) throws ServiceException 071 { 072 super.service(smanager); 073 _subscribersDao = (SubscribersDAO) smanager.lookup(SubscribersDAO.ROLE); 074 _categoryProviderEP = (CategoryProviderExtensionPoint) smanager.lookup(CategoryProviderExtensionPoint.ROLE); 075 } 076 077 @Override 078 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 079 { 080 Map<String, Object> result = new HashMap<>(); 081 082 Request request = ObjectModelHelper.getRequest(objectModel); 083 084 String siteName = parameters.getParameter("siteName", request.getParameter("siteName")); 085 String categoryId = request.getParameter("categoryId"); 086 boolean cleanTable = "true".equals(request.getParameter("cleanSubscribers")); 087 088 Part part = (Part) request.get("importFile"); 089 if (part instanceof RejectedPart) 090 { 091 request.setAttribute(JSonReader.OBJECT_TO_READ, Map.of("success", false, "error", "rejected-file")); 092 return EMPTY_MAP; 093 } 094 095 PartOnDisk uploadedFilePart = (PartOnDisk) part; 096 File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null; 097 String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null; 098 099 if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS)) 100 { 101 request.setAttribute(JSonReader.OBJECT_TO_READ, Map.of("success", false, "error", "invalid-extension")); 102 return EMPTY_MAP; 103 } 104 105 // Test if the category exists. 106 Category category = _categoryProviderEP.getCategory(categoryId); 107 if (category == null) 108 { 109 request.setAttribute(JSonReader.OBJECT_TO_READ, Map.of("success", false, "error", "unknown-category")); 110 return EMPTY_MAP; 111 } 112 113 FileInputStream fileIS = new FileInputStream(uploadedFile); 114 BOMInputStream bomIS = new BOMInputStream(fileIS); 115 116 // Extract the emails from the import file. 117 Collection<String> emails = getEmails(bomIS); 118 119 if (!emails.isEmpty()) 120 { 121 // Empty the category subscribers if needed. 122 if (cleanTable) 123 { 124 _subscribersDao.empty(categoryId, siteName); 125 } 126 127 // Insert the emails. 128 insertSubscribers(emails, categoryId, siteName, result); 129 } 130 131 result.put("success", true); 132 result.put("categoryId", categoryId); 133 134 request.setAttribute(JSonReader.OBJECT_TO_READ, result); 135 return EMPTY_MAP; 136 } 137 138 /** 139 * Extract the emails from the file. 140 * @param bomIS the input stream 141 * @return a collection of the emails. 142 * @throws IOException if an error occurs reading the file. 143 */ 144 protected Collection<String> getEmails(BOMInputStream bomIS) throws IOException 145 { 146 Set<String> emails = new LinkedHashSet<>(); 147 148 for (String line : IOUtils.readLines(bomIS, StandardCharsets.UTF_8)) 149 { 150 String[] part = line.split(";"); 151 String email = StringUtils.trimToEmpty(part[0]); 152 if (__EMAIL_VALIDATOR.matcher(email.toLowerCase()).matches() && StringUtils.isNotBlank(email)) 153 { 154 emails.add(email); 155 } 156 } 157 158 return emails; 159 } 160 161 /** 162 * Insert subscribers 163 * @param emails the emails to insert 164 * @param categoryId the id of category 165 * @param siteName the site name of category 166 * @param result the result map 167 */ 168 protected void insertSubscribers(Collection<String> emails, String categoryId, String siteName, Map<String, Object> result) 169 { 170 int subscribedCount = 0; 171 int existingCount = 0; 172 int errorCount = 0; 173 174 for (String email : emails) 175 { 176 try 177 { 178 if (_subscribersDao.getSubscriber(email, siteName, categoryId) == null) 179 { 180 Subscriber subscriber = new Subscriber(); 181 subscriber.setEmail(email); 182 subscriber.setSiteName(siteName); 183 subscriber.setCategoryId(categoryId); 184 subscriber.setSubscribedAt(new Date()); 185 186 // Generate unique token. 187 String token = UUID.randomUUID().toString(); 188 subscriber.setToken(token); 189 190 _subscribersDao.subscribe(subscriber); 191 192 if (getLogger().isInfoEnabled()) 193 { 194 getLogger().info("The user with email '" + email + "' subscribed to the newsletter with the token " + token); 195 } 196 197 subscribedCount++; 198 } 199 else 200 { 201 existingCount++; 202 } 203 } 204 catch (Exception e) 205 { 206 getLogger().error("Impossible to add as a subscriber the email " + email + " in category " + categoryId + " of site " + siteName, e); 207 errorCount++; 208 } 209 } 210 211 result.put("subscribedCount", Integer.toString(subscribedCount)); 212 result.put("existingCount", Integer.toString(existingCount)); 213 result.put("errorCount", Integer.toString(errorCount)); 214 } 215 216}