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, String language) 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, language); 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 * @param language The language to use 226 * @return the HTML mail body 227 * @throws IOException if failed to build send invitation report 228 */ 229 protected String _buildMailBody(JobExecutionContext context, List<String> successEmails, List<String> invalidEmails, List<String> errorMails, List<String> existingTempUsers, List<String> existingUsers, String language) throws IOException 230 { 231 Site site = _getSite(context); 232 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 233 234 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 235 236 i18nParams.put("siteTitle", new I18nizableText(siteTitle)); 237 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 238 i18nParams.put("count", new I18nizableText(String.valueOf(successEmails.size()))); 239 240 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 241 .withTitle(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_TITLE")) 242 .withLanguage(language); 243 244 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY", i18nParams)); 245 246 if (!invalidEmails.isEmpty()) 247 { 248 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 249 errorI18nParams.put("count", new I18nizableText(String.valueOf(invalidEmails.size()))); 250 251 List<String> formatedEmails = invalidEmails.stream() 252 .map(email -> "<strong>" + email + "</strong>") 253 .toList(); 254 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 255 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_INVALID_EMAILS_ERROR", errorI18nParams)); 256 } 257 258 if (!errorMails.isEmpty()) 259 { 260 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 261 errorI18nParams.put("count", new I18nizableText(String.valueOf(errorMails.size()))); 262 List<String> formatedEmails = errorMails.stream() 263 .map(email -> "<strong>" + email + "</strong>") 264 .toList(); 265 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 266 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_MAILS_ERROR", errorI18nParams)); 267 } 268 269 if (!existingTempUsers.isEmpty()) 270 { 271 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 272 errorI18nParams.put("count", new I18nizableText(String.valueOf(existingTempUsers.size()))); 273 List<String> formatedEmails = existingTempUsers.stream() 274 .map(email -> "<strong>" + email + "</strong>") 275 .toList(); 276 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 277 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_TEMP_USERS_EXIST_ERROR", errorI18nParams)); 278 } 279 280 if (!existingUsers.isEmpty()) 281 { 282 Map<String, I18nizableTextParameter> errorI18nParams = new HashMap<>(); 283 errorI18nParams.put("count", new I18nizableText(String.valueOf(existingUsers.size()))); 284 List<String> formatedEmails = existingUsers.stream() 285 .map(email -> "<strong>" + email + "</strong>") 286 .toList(); 287 errorI18nParams.put("mails", new I18nizableText(StringUtils.join(formatedEmails, "<br/>"))); 288 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_USERS_EXIST_ERROR", errorI18nParams)); 289 } 290 291 // TODO check user rights 292 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_SUCCESS_MAIL_BODY_END", Map.of("cmsToolUri", new I18nizableText(getToolUri(site))))); 293 294 return bodyBuilder.build(); 295 } 296 297 /** 298 * Get the back-office url to access user temp tool 299 * @param site the site 300 * @return the tool uri 301 */ 302 protected String getToolUri(Site site) 303 { 304 StringBuilder url = new StringBuilder(_cmsUrl); 305 306 url.append("/" + site.getName()); 307 url.append("/index.html?uitool=uitool-temp-users"); 308 309 return url.toString(); 310 } 311 312 @Override 313 protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception 314 { 315 Site site = _getSite(context); 316 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 317 return new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_SUBJECT", List.of(siteTitle)); 318 } 319 320 @Override 321 protected String _getErrorMailBody(JobExecutionContext context, String language, Throwable throwable) throws Exception 322 { 323 StatusError errorCause = (StatusError) context.get(__GLOBAL_ERROR_CAUSE_KEY); 324 325 return _buildErrorMailBody(context, errorCause, language); 326 } 327 328 /** 329 * Build the HTML mail body in case of error 330 * 331 * @param context the job context data 332 * @param globalErrorCause the global error cause 333 * @param language The language to use 334 * @return the HTML mail body in case of error 335 * @throws IOException if failed to build send invitation error 336 */ 337 protected String _buildErrorMailBody(JobExecutionContext context, StatusError globalErrorCause, String language) throws IOException 338 { 339 Site site = _getSite(context); 340 String siteTitle = site.getTitle() != null ? site.getTitle() : site.getName(); 341 342 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 343 344 i18nParams.put("siteTitle", new I18nizableText(siteTitle)); 345 i18nParams.put("siteUrl", new I18nizableText(site.getUrl())); 346 347 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 348 .withTitle(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_TITLE")) 349 .withLanguage(language); 350 351 bodyBuilder.addMessage(new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY", i18nParams)); 352 353 if (globalErrorCause != null) 354 { 355 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 356 357 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(jobDataMap.getString(__JOBDATAMAP_USER_POPULATION_ID_KEY)); 358 359 switch (globalErrorCause) 360 { 361 case NO_SIGNUP_PAGE: 362 i18nParams.put("population", userPopulation.getLabel()); 363 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); 364 break; 365 case SIGNUP_NOT_ALLOWED: 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_NO_SIGNUP_ALLOWED_ERROR"), false); 367 break; 368 case USER_NOT_ALLOWED: 369 i18nParams.put("population", userPopulation.getLabel()); 370 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); 371 break; 372 default: 373 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); 374 break; 375 } 376 } 377 else 378 { 379 bodyBuilder.withDetails(null, new I18nizableText("plugin.web", "PLUGINS_WEB_USERS_SEND_INVITATIONS_ERROR_MAIL_BODY_SEE_LOGS"), false); 380 } 381 382 return bodyBuilder.build(); 383 } 384 385 /** 386 * Get the site title from job execution context 387 * 388 * @param context the job context 389 * @return the site title 390 */ 391 protected Site _getSite(JobExecutionContext context) 392 { 393 JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); 394 String siteName = (String) jobDataMap.get(__JOBDATAMAP_SITE_NAME_KEY); 395 396 return _siteManager.getSite(siteName); 397 } 398 399}