001/* 002 * Copyright 2023 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.web.usermanagement; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.cocoon.components.ContextHelper; 028import org.apache.cocoon.environment.Request; 029import org.apache.commons.lang3.StringUtils; 030import org.quartz.JobDataMap; 031import org.quartz.JobExecutionContext; 032 033import org.ametys.cms.schedule.AbstractSendingMailSchedulable; 034import org.ametys.core.schedule.progression.ContainerProgressionTracker; 035import org.ametys.core.ui.mail.StandardMailBodyHelper; 036import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder; 037import org.ametys.core.user.population.UserPopulation; 038import org.ametys.core.user.population.UserPopulationDAO; 039import org.ametys.core.util.HttpUtils; 040import org.ametys.plugins.core.schedule.Scheduler; 041import org.ametys.runtime.config.Config; 042import org.ametys.runtime.i18n.I18nizableText; 043import org.ametys.runtime.i18n.I18nizableTextParameter; 044import org.ametys.web.WebConstants; 045import org.ametys.web.repository.site.Site; 046import org.ametys.web.repository.site.SiteManager; 047import org.ametys.web.usermanagement.UserManagementException.StatusError; 048 049/** 050 * Job for sending invitations email 051 */ 052public class SendInvitationsSchedulable extends AbstractSendingMailSchedulable 053{ 054 /** The key for the id of the population */ 055 public static final String USER_POPULATION_ID_KEY = "populationId"; 056 057 /** The key for the id of the population */ 058 public static final String USER_DIRECTORY_ID_KEY = "userDirectoryId"; 059 060 /** The key for the site name */ 061 public static final String SITE_NAME_KEY = "siteName"; 062 063 /** The key for the guests */ 064 public static final String GUESTS_KEY = "guests"; 065 066 /** The key for the guests */ 067 public static final String RESEND_INVITATIONS_KEY = "resendInvitations"; 068 069 private static final String __JOBDATAMAP_USER_POPULATION_ID_KEY = Scheduler.PARAM_VALUES_PREFIX + USER_POPULATION_ID_KEY; 070 071 private static final String __JOBDATAMAP_USER_DIRECTORY_ID_KEY = Scheduler.PARAM_VALUES_PREFIX + USER_DIRECTORY_ID_KEY; 072 073 private static final String __JOBDATAMAP_SITE_NAME_KEY = Scheduler.PARAM_VALUES_PREFIX + SITE_NAME_KEY; 074 075 private static final String __JOBDATAMAP_GUESTS_KEY = Scheduler.PARAM_VALUES_PREFIX + GUESTS_KEY; 076 077 private static final String __JOBDATAMAP_RESEND_INVITATIONS_KEY = Scheduler.PARAM_VALUES_PREFIX + RESEND_INVITATIONS_KEY; 078 079 private static final String __GLOBAL_ERROR_CAUSE_KEY = SendInvitationsSchedulable.class.getName() + "$globalError"; 080 081 private static final String __INVALID_EMAILS_KEY = SendInvitationsSchedulable.class.getName() + "$invalidEmails"; 082 083 private static final String __ERROR_MAILS_KEY = SendInvitationsSchedulable.class.getName() + "$errorMails"; 084 085 private static final String __SUCCESS_EMAILS_KEY = SendInvitationsSchedulable.class.getName() + "$successEmails"; 086 087 private static final String __EXISTING_USERS_KEY = SendInvitationsSchedulable.class.getName() + "$existingUsers"; 088 089 private static final String __EXISTING_TEMP_USERS_KEY = SendInvitationsSchedulable.class.getName() + "$existingTempUsers"; 090 091 /** The signup manager */ 092 protected UserSignupManager _signupManager; 093 /** The site manager */ 094 protected SiteManager _siteManager; 095 /** DAO for user population */ 096 protected UserPopulationDAO _userPopulationDAO; 097 /** The CMS base url */ 098 private String _cmsUrl; 099 100 @Override 101 public void service(ServiceManager smanager) throws ServiceException 102 { 103 super.service(smanager); 104 _signupManager = (UserSignupManager) smanager.lookup(UserSignupManager.ROLE); 105 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 106 _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE); 107 } 108 109 @Override 110 public void initialize() throws Exception 111 { 112 super.initialize(); 113 _cmsUrl = HttpUtils.sanitize(Config.getInstance().getValue("cms.url")); 114 } 115 116 @Override 117 protected void _doExecute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 118 { 119 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 120 121 String populationId = jobDataMap.getString(__JOBDATAMAP_USER_POPULATION_ID_KEY); 122 String userDirectoryId = jobDataMap.getString(__JOBDATAMAP_USER_DIRECTORY_ID_KEY); 123 String siteName = jobDataMap.getString(__JOBDATAMAP_SITE_NAME_KEY); 124 boolean resendInvitations = jobDataMap.getBooleanValue(__JOBDATAMAP_RESEND_INVITATIONS_KEY); 125 126 @SuppressWarnings("unchecked") 127 List<String> guestUserLines = (List<String>) jobDataMap.get(__JOBDATAMAP_GUESTS_KEY); 128 129 List<String> successEmails = new ArrayList<>(); 130 List<String> invalidEmails = new ArrayList<>(); 131 List<String> errorMails = new ArrayList<>(); 132 List<String> existingUsers = new ArrayList<>(); 133 List<String> existingTempUsers = new ArrayList<>(); 134 135 Request request = ContextHelper.getRequest(_context); 136 137 // Set the site name to be able to check rights 138 request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, siteName); 139 140 for (String line : guestUserLines) 141 { 142 String[] columns = Arrays.stream(line.split(";")).map(StringUtils::normalizeSpace).toArray(String[]::new); 143 144 String email = columns[0]; 145 String lastname = columns.length > 1 ? columns[1] : null; 146 String firstname = columns.length > 2 ? columns[2] : null; 147 148 try 149 { 150 _signupManager.inviteToSignup(siteName, null, email, populationId, userDirectoryId, lastname, firstname, true, resendInvitations, true); 151 successEmails.add(email); 152 } 153 catch (UserManagementException e) 154 { 155 StatusError errorCause = e.getStatusError(); 156 switch (errorCause) 157 { 158 case NO_SIGNUP_PAGE: 159 case SIGNUP_NOT_ALLOWED: 160 case USER_NOT_ALLOWED: 161 context.put(__GLOBAL_ERROR_CAUSE_KEY, errorCause); 162 throw new IllegalArgumentException("Invalid configuration to send invitation", e); 163 case USER_ALREADY_EXISTS: 164 existingUsers.add(email); 165 break; 166 case INVALID_EMAIL: 167 invalidEmails.add(email); 168 break; 169 case MAIL_ERROR: 170 errorMails.add(email); 171 break; 172 case TEMP_USER_ALREADY_EXISTS: 173 existingTempUsers.add(email); 174 break; 175 default: 176 throw new IllegalArgumentException("Unexpected error: " + errorCause, e); 177 } 178 } 179 } 180 181 context.put(__SUCCESS_EMAILS_KEY, successEmails); 182 context.put(__ERROR_MAILS_KEY, errorMails); 183 context.put(__INVALID_EMAILS_KEY, invalidEmails); 184 context.put(__EXISTING_TEMP_USERS_KEY, existingTempUsers); 185 context.put(__EXISTING_USERS_KEY, existingUsers); 186 } 187 188 @Override 189 protected I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception 190 { 191 Site site = _getSite(context); 192 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 193 194 return new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_SUBJECT", List.of(siteTitle)); 195 } 196 197 @SuppressWarnings("unchecked") 198 @Override 199 protected String _getSuccessMailBody(JobExecutionContext context) throws Exception 200 { 201 List<String> successEmails = (List<String>) context.get(__SUCCESS_EMAILS_KEY); 202 List<String> invalidEmails = (List<String>) context.get(__INVALID_EMAILS_KEY); 203 List<String> errorMails = (List<String>) context.get(__ERROR_MAILS_KEY); 204 List<String> existingTempUsers = (List<String>) context.get(__EXISTING_TEMP_USERS_KEY); 205 List<String> existingUsers = (List<String>) context.get(__EXISTING_USERS_KEY); 206 207 return _buildMailBody(context, successEmails, invalidEmails, errorMails, existingTempUsers, existingUsers); 208 } 209 210 @Override 211 protected boolean _isMailBodyInHTML(JobExecutionContext context) throws Exception 212 { 213 return true; 214 } 215 216 /** 217 * Build the HTML mail body 218 * 219 * @param context the job context data 220 * @param successEmails the mails in success 221 * @param invalidEmails the invalid emails 222 * @param errorMails the mails for which the send has failed 223 * @param existingTempUsers the existing temporary users 224 * @param existingUsers the existing users 225 * @return the HTML mail body 226 * @throws IOException if failed to build send invitation report 227 */ 228 protected String _buildMailBody(JobExecutionContext context, List<String> successEmails, List<String> invalidEmails, List<String> errorMails, List<String> existingTempUsers, List<String> existingUsers) throws IOException 229 { 230 Site site = _getSite(context); 231 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 232 233 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 234 235 i18nParams.put("siteTitle", new I18nizableText(siteTitle)); 236 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 237 i18nParams.put("count", new I18nizableText(String.valueOf(successEmails.size()))); 238 239 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 240 .withTitle(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_TITLE")); 241 242 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY", i18nParams)); 243 244 if (!invalidEmails.isEmpty()) 245 { 246 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 247 errorI18nParams.put("count", new I18nizableText(String.valueOf(invalidEmails.size()))); 248 249 List<String> formatedEmails = invalidEmails.stream() 250 .map(email -> "<strong>" + email + "</strong>") 251 .toList(); 252 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 253 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_INVALID_EMAILS_ERROR", errorI18nParams)); 254 } 255 256 if (!errorMails.isEmpty()) 257 { 258 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 259 errorI18nParams.put("count", new I18nizableText(String.valueOf(errorMails.size()))); 260 List<String> formatedEmails = errorMails.stream() 261 .map(email -> "<strong>" + email + "</strong>") 262 .toList(); 263 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 264 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_MAILS_ERROR", errorI18nParams)); 265 } 266 267 if (!existingTempUsers.isEmpty()) 268 { 269 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 270 errorI18nParams.put("count", new I18nizableText(String.valueOf(existingTempUsers.size()))); 271 List<String> formatedEmails = existingTempUsers.stream() 272 .map(email -> "<strong>" + email + "</strong>") 273 .toList(); 274 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 275 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_TEMP_USERS_EXIST_ERROR", errorI18nParams)); 276 } 277 278 if (!existingUsers.isEmpty()) 279 { 280 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 281 errorI18nParams.put("count", new I18nizableText(String.valueOf(existingUsers.size()))); 282 List<String> formatedEmails = existingUsers.stream() 283 .map(email -> "<strong>" + email + "</strong>") 284 .toList(); 285 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 286 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_USERS_EXIST_ERROR", errorI18nParams)); 287 } 288 289 // TODO check user rights 290 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_END", Map.of("cmsToolUri", new I18nizableText(getToolUri(site))))); 291 292 return bodyBuilder.build(); 293 } 294 295 /** 296 * Get the back-office url to access user temp tool 297 * @param site the site 298 * @return the tool uri 299 */ 300 protected String getToolUri(Site site) 301 { 302 StringBuilder url = new StringBuilder(_cmsUrl); 303 304 url.append("/" + site.getName()); 305 url.append("/index.html?uitool=uitool-temp-users"); 306 307 return url.toString(); 308 } 309 310 @Override 311 protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception 312 { 313 Site site = _getSite(context); 314 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 315 return new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_SUBJECT", List.of(siteTitle)); 316 } 317 318 @Override 319 protected String _getErrorMailBody(JobExecutionContext context, Throwable throwable) throws Exception 320 { 321 StatusError errorCause = (StatusError) context.get(__GLOBAL_ERROR_CAUSE_KEY); 322 323 return _buildErrorMailBody(context, errorCause); 324 } 325 326 /** 327 * Build the HTML mail body in case of error 328 * 329 * @param context the job context data 330 * @param globalErrorCause the global error cause 331 * @return the HTML mail body in case of error 332 * @throws IOException if failed to build send invitation error 333 */ 334 protected String _buildErrorMailBody(JobExecutionContext context, StatusError globalErrorCause) throws IOException 335 { 336 Site site = _getSite(context); 337 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 338 339 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 340 341 i18nParams.put("siteTitle", new I18nizableText(siteTitle)); 342 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 343 344 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 345 .withTitle(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_TITLE")); 346 347 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY", i18nParams)); 348 349 if (globalErrorCause != null) 350 { 351 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 352 353 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(jobDataMap.getString(__JOBDATAMAP_USER_POPULATION_ID_KEY)); 354 355 switch (globalErrorCause) 356 { 357 case NO_SIGNUP_PAGE: 358 i18nParams.put("population", userPopulation.getLabel()); 359 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_NO_SIGNUP_PAGE_ERROR", i18nParams), false); 360 break; 361 case SIGNUP_NOT_ALLOWED: 362 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_NO_SIGNUP_ALLOWED_ERROR"), false); 363 break; 364 case USER_NOT_ALLOWED: 365 i18nParams.put("population", userPopulation.getLabel()); 366 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_USER_NOT_ALLOWED_ERROR", i18nParams), false); 367 break; 368 default: 369 bodyBuilder.withDetails(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_ERROR_TITLE"), new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_SEE_LOGS"), false); 370 break; 371 } 372 } 373 else 374 { 375 bodyBuilder.withDetails(null, new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_SEE_LOGS"), false); 376 } 377 378 return bodyBuilder.build(); 379 } 380 381 /** 382 * Get the site title from job execution context 383 * 384 * @param context the job context 385 * @return the site title 386 */ 387 protected Site _getSite(JobExecutionContext context) 388 { 389 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 390 String siteName = (String) jobDataMap.get(__JOBDATAMAP_SITE_NAME_KEY); 391 392 return _siteManager.getSite(siteName); 393 } 394 395}