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}