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