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