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.Collections; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.LinkedHashSet; 027import java.util.Map; 028import java.util.Set; 029import java.util.UUID; 030import java.util.regex.Pattern; 031 032import org.apache.avalon.framework.parameters.Parameters; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.cocoon.acting.ServiceableAction; 036import org.apache.cocoon.environment.ObjectModelHelper; 037import org.apache.cocoon.environment.Redirector; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.environment.SourceResolver; 040import org.apache.cocoon.servlet.multipart.Part; 041import org.apache.cocoon.servlet.multipart.PartOnDisk; 042import org.apache.cocoon.servlet.multipart.RejectedPart; 043import org.apache.commons.io.FilenameUtils; 044import org.apache.commons.io.IOUtils; 045import org.apache.commons.io.input.BOMInputStream; 046import org.apache.commons.lang.StringUtils; 047 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, String> 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 return Collections.singletonMap("error", "rejected-file"); 092 } 093 094 PartOnDisk uploadedFilePart = (PartOnDisk) part; 095 File uploadedFile = (uploadedFilePart != null) ? uploadedFilePart.getFile() : null; 096 String filename = (uploadedFilePart != null) ? uploadedFilePart.getFileName().toLowerCase() : null; 097 098 if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS)) 099 { 100 return Collections.singletonMap("error", "invalid-extension"); 101 } 102 103 // Test if the category exists. 104 Category category = _categoryProviderEP.getCategory(categoryId); 105 if (category == null) 106 { 107 return Collections.singletonMap("error", "unknown-category"); 108 } 109 110 FileInputStream fileIS = new FileInputStream(uploadedFile); 111 BOMInputStream bomIS = new BOMInputStream(fileIS); 112 113 // Extract the emails from the import file. 114 Collection<String> emails = getEmails(bomIS); 115 116 if (!emails.isEmpty()) 117 { 118 // Empty the category subscribers if needed. 119 if (cleanTable) 120 { 121 _subscribersDao.empty(categoryId, siteName); 122 } 123 124 // Insert the emails. 125 insertSubscribers(emails, categoryId, siteName, result); 126 } 127 128 result.put("success", "true"); 129 result.put("categoryId", categoryId); 130 131 return result; 132 } 133 134 /** 135 * Extract the emails from the file. 136 * @param bomIS the input stream 137 * @return a collection of the emails. 138 * @throws IOException if an error occurs reading the file. 139 */ 140 protected Collection<String> getEmails(BOMInputStream bomIS) throws IOException 141 { 142 Set<String> emails = new LinkedHashSet<>(); 143 144 for (String line : IOUtils.readLines(bomIS, StandardCharsets.UTF_8)) 145 { 146 String[] part = line.split(";"); 147 String email = StringUtils.trimToEmpty(part[0]); 148 if (__EMAIL_VALIDATOR.matcher(email.toLowerCase()).matches() && StringUtils.isNotBlank(email)) 149 { 150 emails.add(email); 151 } 152 } 153 154 return emails; 155 } 156 157 /** 158 * Insert subscribers 159 * @param emails the emails to insert 160 * @param categoryId the id of category 161 * @param siteName the site name of category 162 * @param result the result map 163 */ 164 protected void insertSubscribers(Collection<String> emails, String categoryId, String siteName, Map<String, String> result) 165 { 166 int subscribedCount = 0; 167 int existingCount = 0; 168 int errorCount = 0; 169 170 for (String email : emails) 171 { 172 try 173 { 174 if (_subscribersDao.getSubscriber(email, siteName, categoryId) == null) 175 { 176 Subscriber subscriber = new Subscriber(); 177 subscriber.setEmail(email); 178 subscriber.setSiteName(siteName); 179 subscriber.setCategoryId(categoryId); 180 subscriber.setSubscribedAt(new Date()); 181 182 // Generate unique token. 183 String token = UUID.randomUUID().toString(); 184 subscriber.setToken(token); 185 186 _subscribersDao.subscribe(subscriber); 187 188 if (getLogger().isInfoEnabled()) 189 { 190 getLogger().info("The user with email '" + email + "' subscribed to the newsletter with the token " + token); 191 } 192 193 subscribedCount++; 194 } 195 else 196 { 197 existingCount++; 198 } 199 } 200 catch (Exception e) 201 { 202 getLogger().error("Impossible to add as a subscriber the email " + email + " in category " + categoryId + " of site " + siteName, e); 203 errorCount++; 204 } 205 } 206 207 result.put("subscribedCount", Integer.toString(subscribedCount)); 208 result.put("existingCount", Integer.toString(existingCount)); 209 result.put("errorCount", Integer.toString(errorCount)); 210 } 211 212}