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}