001/*
002 *  Copyright 2018 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.plugins.workspaces.members;
017
018import java.io.IOException;
019import java.io.UnsupportedEncodingException;
020import java.net.URLEncoder;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import javax.jcr.RepositoryException;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.configuration.Configurable;
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.context.Context;
036import org.apache.avalon.framework.context.ContextException;
037import org.apache.avalon.framework.context.Contextualizable;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.components.ContextHelper;
042import org.apache.cocoon.environment.Request;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.lang3.Strings;
045
046import org.ametys.cms.fo.ForceDefaultRepositoryWorkspaceCallableDecorator;
047import org.ametys.cms.languages.Language;
048import org.ametys.cms.languages.LanguagesManager;
049import org.ametys.cms.transformation.xslt.ResolveURIComponent;
050import org.ametys.core.ui.Callable;
051import org.ametys.core.ui.mail.StandardMailBodyHelper;
052import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
053import org.ametys.core.user.CurrentUserProvider;
054import org.ametys.core.user.User;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.core.user.UserManager;
057import org.ametys.core.user.directory.NotUniqueUserException;
058import org.ametys.core.user.directory.UserDirectory;
059import org.ametys.core.user.population.PopulationContextHelper;
060import org.ametys.core.util.I18nUtils;
061import org.ametys.core.util.language.UserLanguagesManager;
062import org.ametys.core.util.mail.SendMailHelper;
063import org.ametys.plugins.core.user.UserHelper;
064import org.ametys.plugins.repository.AmetysObjectIterable;
065import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
066import org.ametys.plugins.workspaces.ObservationConstants;
067import org.ametys.plugins.workspaces.WorkspacesHelper;
068import org.ametys.plugins.workspaces.members.MembersWorkspaceModule.Invitation;
069import org.ametys.plugins.workspaces.project.ProjectManager;
070import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
071import org.ametys.plugins.workspaces.project.objects.Project;
072import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
073import org.ametys.runtime.authentication.AccessDeniedException;
074import org.ametys.runtime.i18n.I18nizableText;
075import org.ametys.runtime.i18n.I18nizableTextParameter;
076import org.ametys.runtime.plugin.component.AbstractLogEnabled;
077import org.ametys.runtime.plugin.component.PluginAware;
078import org.ametys.web.WebConstants;
079import org.ametys.web.WebHelper;
080import org.ametys.web.repository.page.Page;
081import org.ametys.web.repository.page.Zone;
082import org.ametys.web.repository.page.ZoneItem;
083import org.ametys.web.repository.site.Site;
084import org.ametys.web.repository.site.SiteManager;
085import org.ametys.web.usermanagement.UserManagementException;
086import org.ametys.web.usermanagement.UserManagementException.StatusError;
087import org.ametys.web.usermanagement.UserSignUpConfiguration;
088import org.ametys.web.usermanagement.UserSignupManager;
089
090import jakarta.mail.MessagingException;
091
092/**
093 * Helper for invitations by email
094 *
095 */
096public class ProjectInvitationHelper extends AbstractLogEnabled implements Serviceable, Component, Configurable, PluginAware, Contextualizable
097{
098    /** The role */
099    public static final String ROLE = ProjectInvitationHelper.class.getName();
100    
101    private static final String __MAIL_PROJECT_EMAIL_PATTERN = "${email}";
102    private static final String __MAIL_PROJECT_TOKEN_PATTERN = "${token}";
103    
104    private ProjectManager _projectManager;
105    private UserSignUpConfiguration _signupConfig;
106    private UserSignupManager _signupManager;
107    private SiteManager _siteManager;
108    private WorkspaceModuleExtensionPoint _moduleEP;
109    private CurrentUserProvider _currentUserProvider;
110    private UserManager _userManager;
111    private UserHelper _userHelper;
112    private ProjectRightHelper _projectRightsHelper;
113    private ProjectMemberManager _projectMemberManager;
114    private PopulationContextHelper _populationContextHelper;
115    private LanguagesManager _languagesManager;
116    private UserLanguagesManager _userLanguagesManager;
117    private WorkspacesHelper _workspacesHelper;
118    
119    private String _subjectKeyForInvitation;
120    private String _textBodyKeyForInvitation;
121    private String _htmlBodyKeyForInvitation;
122    private String _subjectKeyForInvitationAccepted;
123    private String _textBodyKeyForInvitationAccepted;
124    private String _htmlBodyKeyForInvitationAccepted;
125
126    private I18nUtils _i18nUtils;
127
128    private String _pluginName;
129
130    private Context _context;
131
132
133    public void setPluginInfo(String pluginName, String featureName, String id)
134    {
135        _pluginName = pluginName;
136    }
137    
138    public void contextualize(Context context) throws ContextException
139    {
140        _context = context;
141    }
142    
143    @Override
144    public void service(ServiceManager serviceManager) throws ServiceException
145    {
146        _projectManager = (ProjectManager) serviceManager.lookup(ProjectManager.ROLE);
147        _projectRightsHelper = (ProjectRightHelper) serviceManager.lookup(ProjectRightHelper.ROLE);
148        _projectMemberManager = (ProjectMemberManager) serviceManager.lookup(ProjectMemberManager.ROLE);
149        _signupConfig = (UserSignUpConfiguration) serviceManager.lookup(UserSignUpConfiguration.ROLE);
150        _signupManager = (UserSignupManager) serviceManager.lookup(UserSignupManager.ROLE);
151        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
152        _moduleEP = (WorkspaceModuleExtensionPoint) serviceManager.lookup(WorkspaceModuleExtensionPoint.ROLE);
153        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
154        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
155        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
156        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
157        _populationContextHelper = (PopulationContextHelper) serviceManager.lookup(PopulationContextHelper.ROLE);
158        _languagesManager = (LanguagesManager) serviceManager.lookup(LanguagesManager.ROLE);
159        _userLanguagesManager = (UserLanguagesManager) serviceManager.lookup(UserLanguagesManager.ROLE);
160        _workspacesHelper = (WorkspacesHelper) serviceManager.lookup(WorkspacesHelper.ROLE);
161    }
162    
163    @Override
164    public void configure(Configuration configuration) throws ConfigurationException
165    {
166        _subjectKeyForInvitation = configuration.getChild("invitation-email-subject").getValue(null);
167        _textBodyKeyForInvitation = configuration.getChild("invitation-email-text-body").getValue(null);
168        _htmlBodyKeyForInvitation = configuration.getChild("invitation-email-html-body").getValue(null);
169        
170        _subjectKeyForInvitationAccepted = configuration.getChild("invitation-accepted-email-subject").getValue(null);
171        _textBodyKeyForInvitationAccepted = configuration.getChild("invitation-accepted-email-text-body").getValue(null);
172        _htmlBodyKeyForInvitationAccepted = configuration.getChild("invitation-accepted-email-html-body").getValue(null);
173    }
174    
175    /**
176     * Invite emails to be member of a project
177     * @param projectName The project name
178     * @param emails The emails
179     * @param allowedProfileByModule the allowed profiles by module
180     * @return The result
181     * @throws UserManagementException if failed to invit user
182     * @throws NotUniqueUserException if many users match the given email
183     */
184    public Map<String, Object> inviteEmails(String projectName, List<String> emails, Map<String, String> allowedProfileByModule) throws UserManagementException, NotUniqueUserException
185    {
186        Request request = ContextHelper.getRequest(_context);
187        String siteName = WebHelper.getSiteName(request);
188        String sitemapLanguage = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
189        
190        return inviteEmails(projectName, siteName, sitemapLanguage, emails, allowedProfileByModule);
191    }
192    /**
193     * Invite emails to be member of a project
194     * @param projectName The project name
195     * @param siteName The site name
196     * @param lang the current language
197     * @param emails The emails
198     * @param allowedProfileByModule the allowed profiles by module
199     * @return The result
200     * @throws UserManagementException if failed to invit user
201     * @throws NotUniqueUserException if many users match the given email
202     */
203    public Map<String, Object> inviteEmails(String projectName, String siteName, String lang, List<String> emails, Map<String, String> allowedProfileByModule) throws UserManagementException, NotUniqueUserException
204    {
205        Map<String, Object> result = new HashMap<>();
206        
207        result.put("existing-users", new ArrayList<>());
208        result.put("email-success", new ArrayList<>());
209        result.put("email-error", new ArrayList<>());
210        
211        Project project = _projectManager.getProject(projectName);
212        if (project != null)
213        {
214            MembersWorkspaceModule module = _moduleEP.getModule(MembersWorkspaceModule.MEMBERS_MODULE_ID);
215            if (module != null && _projectManager.isModuleActivated(project, module.getId()))
216            {
217                String language = _workspacesHelper.getLang(project, lang);
218                
219                if (!_projectRightsHelper.canAddMember(project))
220                {
221                    throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to send invitations without sufficient right.");
222                }
223                
224                String catalogSiteName = _projectManager.getCatalogSiteName();
225                
226                UserDirectory userDirectory = _getUserDirectoryForSignup(result, catalogSiteName, language);
227                if (userDirectory != null)
228                {
229                    String populationId = userDirectory.getPopulationId();
230                    String userDirectoryId = userDirectory.getId();
231                    
232                    // Prepare mail
233                    Map<String, String> emailConfiguration = _getInvitationEmailConfiguration(project, language);
234                    String mailSubject = emailConfiguration.get("subject");
235                    String mailBodyText = emailConfiguration.get("bodyText");
236                    String mailBodyHtml = emailConfiguration.get("bodyHtml");
237                    
238                    for (String email : emails)
239                    {
240                        try
241                        {
242                            _signupManager.inviteToSignup(catalogSiteName, language, email, populationId, userDirectoryId, null, null, false, false, false);
243                            
244                            if (_addOrUpdateInvitation(project, catalogSiteName, module, email, allowedProfileByModule, populationId, userDirectoryId, mailSubject, mailBodyText, mailBodyHtml))
245                            {
246                                @SuppressWarnings("unchecked")
247                                List<String> emailSuccess = (List<String>) result.get("email-success");
248                                emailSuccess.add(email);
249                                
250                            }
251                        }
252                        catch (UserManagementException e)
253                        {
254                            switch (e.getStatusError())
255                            {
256                                case USER_ALREADY_EXISTS:
257                                    User user = _getUser(catalogSiteName, email);
258                                    
259                                    Map<String, Object> user2json = _userHelper.user2json(user, true);
260                                    
261                                    @SuppressWarnings("unchecked")
262                                    List<Map<String, Object>> existingUsers = (List<Map<String, Object>>) result.get("existing-users");
263                                    existingUsers.add(user2json);
264                                    break;
265                                    
266                                case TEMP_USER_ALREADY_EXISTS:
267                                    // if mail has already been invited, re-created the invitation
268                                    if (_addOrUpdateInvitation(project, catalogSiteName, module, email, allowedProfileByModule, populationId, userDirectoryId, mailSubject, mailBodyText, mailBodyHtml))
269                                    {
270                                        @SuppressWarnings("unchecked")
271                                        List<String> emailSuccess = (List<String>) result.get("email-success");
272                                        emailSuccess.add(email);
273                                        
274                                    }
275                                    break;
276                                    
277                                default:
278                                    getLogger().error("Cannot invite " + email, e);
279                                    @SuppressWarnings("unchecked")
280                                    List<String> emailErrors = (List<String>) result.get("email-error");
281                                    emailErrors.add(email);
282                                    break;
283                            }
284                        }
285                    }
286                    
287                    @SuppressWarnings("unchecked")
288                    List<String> emailSuccess = (List<String>) result.get("email-success");
289                    if (emailSuccess.size() == emails.size())
290                    {
291                        result.put("success", true);
292                    }
293                }
294            }
295        }
296        else
297        {
298            result.put("success", false);
299            result.put("unknown-project", projectName);
300        }
301        
302        return result;
303    }
304    
305    private User _getUser(String siteName, String email) throws NotUniqueUserException
306    {
307        Set<String> populations = _populationContextHelper.getUserPopulationsOnContexts(Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName), false);
308        for (String population : populations)
309        {
310            User user = _userManager.getUser(population, email);
311            if (user == null)
312            {
313                user = _userManager.getUserByEmail(population, email);
314            }
315            
316            if (user != null)
317            {
318                return user;
319            }
320        }
321        
322        return null;
323    }
324    
325    /**
326     * Get the configuration to invite users by emails
327     * @param projectName The current project
328     * @param lang the current language
329     * @return the configuration for email invitations
330     */
331    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
332    public Map<String, Object> getInvitationConfiguration(String projectName, String lang)
333    {
334        Project project = _projectManager.getProject(projectName);
335        
336        Map<String, Object> config = new HashMap<>();
337
338        // Check the current user has right to invite users
339        if (!_projectRightsHelper.canAddMember(project))
340        {
341            config.put("allowed", false);
342            config.put("error", "no-right");
343            return config;
344        }
345        
346        // Check the configuration is valid for invitations
347        String catalogSiteName = _projectManager.getCatalogSiteName();
348        if (_getUserDirectoryForSignup(config, catalogSiteName, lang) == null)
349        {
350            config.remove("success");
351            config.put("allowed", false);
352            return config;
353        }
354        
355        config.put("allowed", true);
356        config.put("email", _getInvitationEmailConfiguration(project, lang));
357        config.put("rights", _projectRightsHelper.getProjectRightsData(projectName));
358        
359        return config;
360    }
361    
362    private Map<String, String> _getInvitationEmailConfiguration(Project project, String lang)
363    {
364        Map<String, String> emailConfig = new HashMap<>();
365        
366        Map<String, I18nizableTextParameter> i18nparams = new HashMap<>();
367        i18nparams.put("projectTitle", new I18nizableText(project.getTitle()));
368        i18nparams.put("projectUrl", new I18nizableText(_projectManager.getProjectUrl(project, StringUtils.EMPTY)));
369        i18nparams.put("nbDays", new I18nizableText(String.valueOf(_signupConfig.getTokenValidity())));
370        i18nparams.put("nbDays", new I18nizableText(String.valueOf(_signupConfig.getTokenValidity())));
371        
372        String catalogSiteName = _projectManager.getCatalogSiteName();
373        Site catalogSite = _siteManager.getSite(catalogSiteName);
374        i18nparams.put("catalogSiteTitle", new I18nizableText(catalogSite.getTitle()));
375        i18nparams.put("catalogSiteUrl", new I18nizableText(catalogSite.getUrl()));
376        
377        Page signupPage = _getSignupPage(_projectManager.getCatalogSiteName(), lang);
378        if (signupPage != null)
379        {
380            String signupUri = ResolveURIComponent.resolve("page", signupPage.getId(), false, true) + "?email=${email}&token=${token}";
381            i18nparams.put("signupUri", new I18nizableText(signupUri));
382        }
383        
384        String subject = getSubjectForInvitationEmail(i18nparams, lang);
385        if (subject != null)
386        {
387            emailConfig.put("subject", subject);
388        }
389        String bodyTxt = getTextBodyForInvitationEmail(i18nparams, lang);
390        if (bodyTxt != null)
391        {
392            emailConfig.put("bodyText", bodyTxt);
393        }
394        String bodyHtml = getHtmlBodyForInvitationEmail(i18nparams, lang);
395        if (bodyHtml != null)
396        {
397            emailConfig.put("bodyHtml", bodyHtml);
398        }
399        
400        return emailConfig;
401    }
402
403    private Page _getSignupPage(String catalogSiteName, String lang)
404    {
405        Page signupPage = _signupManager.getSignupPage(catalogSiteName, lang);
406
407        if (signupPage == null)
408        {
409            signupPage = _signupManager.getSignupPage(catalogSiteName, "en");
410        }
411        
412        if (signupPage == null)
413        {
414            Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
415            for (Language availableLanguage : availableLanguages.values())
416            {
417                if (signupPage == null)
418                {
419                    signupPage = _signupManager.getSignupPage(catalogSiteName, availableLanguage.getCode());
420                }
421            }
422        }
423        return signupPage;
424    }
425    
426    private void _sendInvitationMail (String catalogSiteName, String email, String populationId, String userDirectoryId, String mailSubject, String mailBodyText, String mailBodyHtml) throws UserManagementException
427    {
428        String encodedEmail;
429        try
430        {
431            encodedEmail = URLEncoder.encode(email, "UTF-8");
432        }
433        catch (UnsupportedEncodingException e)
434        {
435            // Should never happen.
436            throw new UserManagementException("Encoding error while sending a sign-up confirmation e-mail.", StatusError.MAIL_ERROR, e);
437        }
438
439        String token = _signupManager.getToken(catalogSiteName, email, populationId, userDirectoryId);
440        
441        String bodyTxt = Strings.CS.replace(mailBodyText, __MAIL_PROJECT_TOKEN_PATTERN, token);
442        bodyTxt = Strings.CS.replace(bodyTxt, __MAIL_PROJECT_EMAIL_PATTERN, encodedEmail);
443        
444        String bodyHtml = Strings.CS.replace(mailBodyHtml, __MAIL_PROJECT_TOKEN_PATTERN, token);
445        bodyHtml = Strings.CS.replace(bodyHtml, __MAIL_PROJECT_EMAIL_PATTERN, encodedEmail);
446        
447        getLogger().debug("Sending signup invitation e-mail to {}", email);
448        
449        Site site = _siteManager.getSite(catalogSiteName);
450        String from = site.getValue("site-mail-from");
451
452        try
453        {
454            List<String> errorReport = new ArrayList<>();
455            
456            // Send the e-mail.
457            SendMailHelper.newMail()
458                          .withSubject(mailSubject)
459                          .withTextBody(bodyTxt)
460                          .withHTMLBody(bodyHtml)
461                          .withSender(from)
462                          .withRecipient(email)
463                          .withErrorReport(errorReport)
464                          .sendMail();
465            
466            if (errorReport.contains(email))
467            {
468                throw new UserManagementException("Error sending the sign-up confirmation mail.", StatusError.MAIL_ERROR);
469            }
470        }
471        catch (MessagingException | IOException e)
472        {
473            throw new UserManagementException("Error sending the sign-up confirmation mail.", StatusError.MAIL_ERROR, e);
474        }
475    }
476    
477    private boolean _addOrUpdateInvitation(Project project, String catalogSiteName, MembersWorkspaceModule module, String email, Map<String, String> allowedProfileByModules, String populationId, String userDirectoryId, String mailSubject, String mailBodyText, String mailBodyHtml) throws UserManagementException
478    {
479        try
480        {
481            // First remove invitation if exists
482            module.removeInvitation(project, email);
483            
484            // Add invitations with temporary rights
485            module.addInvitation(project, new Date(), email, _currentUserProvider.getUser(), allowedProfileByModules);
486            
487            project.saveChanges();
488
489            _sendInvitationMail(catalogSiteName, email, populationId, userDirectoryId, mailSubject, mailBodyText, mailBodyHtml);
490            
491            return true;
492        }
493        catch (RepositoryException e)
494        {
495            getLogger().error("Fail to store invitation for email " + email, e);
496            return false;
497        }
498    }
499    
500    private UserDirectory _getUserDirectoryForSignup(Map<String, Object> result, String catalogSiteName, String lang)
501    {
502        if (catalogSiteName == null)
503        {
504            getLogger().error("The catalog's site name is not configured. User invitations can not be activated.");
505            result.put("success", false);
506            result.put("error", "invalid-configuration");
507            return null;
508        }
509        
510        if (!_signupManager.isSignupAllowed(catalogSiteName))
511        {
512            getLogger().warn("Signup is disabled for the catalog's site.");
513            result.put("success", false);
514            return null;
515        }
516        
517        Page signupPage = _getSignupPage(catalogSiteName, lang);
518        if (signupPage == null)
519        {
520            getLogger().error("The catalog's site does not contain the signup service for language " + lang + ". User invitations can not be activated.");
521            result.put("success", false);
522            result.put("error", "invalid-configuration");
523            return null;
524        }
525        
526        UserDirectory userDirectory = _getUserDirectoryForSignup(signupPage);
527        if (userDirectory == null)
528        {
529            getLogger().error("There is no user directory configured for users signup. Please check the sign up service of catalog's site.");
530            result.put("success", false);
531            result.put("error", "invalid-configuration");
532            return null;
533        }
534        
535        return userDirectory;
536            
537    }
538    
539    private UserDirectory _getUserDirectoryForSignup(Page signupPage)
540    {
541        for (Zone zone : signupPage.getZones())
542        {
543            try (AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems())
544            {
545                for (ZoneItem zoneItem : zoneItems)
546                {
547                    UserDirectory userDirectory = _signupManager.getUserDirectory(zoneItem);
548                    if (userDirectory != null)
549                    {
550                        return userDirectory;
551                    }
552                }
553            }
554        }
555        
556        return null;
557        
558    }
559    
560    /**
561     * Add user as member of all project where it has been invited
562     * @param user the new user
563     */
564    public void createMemberFromInvitations(User user)
565    {
566        Request request = ContextHelper.getRequest(_context);
567        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
568        
569        try
570        {
571            // Force default workspace
572            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "default");
573            
574            MembersWorkspaceModule memberModule = (MembersWorkspaceModule) _moduleEP.getExtension(MembersWorkspaceModule.MEMBERS_MODULE_ID);
575            
576            List<Invitation> invitations = memberModule.getInvitations(user.getEmail());
577            
578            for (Invitation invitation : invitations)
579            {
580                Map<String, String> allowedProfileByModules = invitation.getAllowedProfileByModules();
581                String projectName = invitation.getProjectName();
582                
583                Project project = _projectManager.getProject(projectName);
584                
585                _projectMemberManager.addOrUpdateProjectMember(project, user.getIdentity(), allowedProfileByModules, invitation.getAuthor(), ObservationConstants.EVENT_MEMBER_JOINED);
586                
587                // Notify author that invitation was accepted
588                UserIdentity author = invitation.getAuthor();
589                sendInvitationAcceptedMail(project, _userManager.getUser(author), user);
590                
591                try
592                {
593                    // Remove invitation
594                    memberModule.removeInvitation(project, invitation.getEmail());
595                }
596                catch (RepositoryException e)
597                {
598                    getLogger().error("Failed to remove invitation " + invitation, e);
599                }
600            }
601        }
602        finally
603        {
604            // Restore current workspace
605            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
606        }
607    
608    }
609    
610    /**
611     * Send email to the user who initiated the invitation
612     * @param project The project
613     * @param invitAuthor The author of invitation
614     * @param newMember The new member
615     */
616    protected void sendInvitationAcceptedMail(Project project, User invitAuthor, User newMember)
617    {
618        if (invitAuthor != null)
619        {
620            Site site = project.getSite();
621            String from = site.getValue("site-mail-from");
622            String email = invitAuthor.getEmail();
623            String language = StringUtils.defaultIfBlank(invitAuthor.getLanguage(), _workspacesHelper.getLang(project, _userLanguagesManager.getDefaultLanguage()));
624            
625            if (StringUtils.isNotEmpty(email))
626            {
627                // Prepare mail.
628                Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
629                i18nParams.put("projectTitle", new I18nizableText(project.getTitle()));
630                i18nParams.put("projectUrl", new I18nizableText(_projectManager.getProjectUrl(project, StringUtils.EMPTY)));
631                i18nParams.put("newMember", new I18nizableText(newMember.getFullName()));
632                i18nParams.put("newMemberMail", new I18nizableText(newMember.getEmail()));
633                
634                Set<Page> memberPages = _projectManager.getModulePages(project, MembersWorkspaceModule.MEMBERS_MODULE_ID);
635                if (!memberPages.isEmpty())
636                {
637                    Page page = memberPages.iterator().next();
638                    i18nParams.put("membersPageUri", new I18nizableText(ResolveURIComponent.resolve("page", page.getId(), false, true)));
639                }
640
641                String subject = getSubjectForInvitationAcceptedEmail(i18nParams, language);
642                String textBody = getTextBodyForInvitationAcceptedEmail(i18nParams, language);
643                String htmlBody = getHtmlBodyForInvitationAcceptedEmail(project, i18nParams, language);
644
645                try
646                {
647                    // Send the e-mail.
648                    SendMailHelper.newMail()
649                                  .withSubject(subject)
650                                  .withHTMLBody(htmlBody)
651                                  .withTextBody(textBody)
652                                  .withSender(from)
653                                  .withRecipient(email)
654                                  .sendMail();
655                }
656                catch (MessagingException | IOException e)
657                {
658                    getLogger().error("Error sending the invitation accepted email.", e);
659                }
660            }
661        }
662    }
663    
664    /**
665     * The email subject for invitation by email
666     * @param defaultI18nParams The default i18n parameters
667     * @param language the language
668     * @return the email subject for invitation by email
669     */
670    public String getSubjectForInvitationEmail (Map<String, I18nizableTextParameter> defaultI18nParams , String language)
671    {
672        if (StringUtils.isNotBlank(_subjectKeyForInvitation))
673        {
674            return _i18nUtils.translate(_getI18nizableText(_subjectKeyForInvitation, defaultI18nParams), language);
675        }
676        
677        return null;
678    }
679
680    /**
681     * The email text body for invitation by email
682     * @param defaultI18nParams The default i18n parameters with :
683     * siteName the site name
684     * email the mail
685     * token the token
686     * confirmUri the confirmation uri
687     * siteTitle the site title
688     * siteUrl the site url
689     * @param language the language
690     * @return the email text for invitation by email
691     */
692    public String getTextBodyForInvitationEmail(Map<String, I18nizableTextParameter> defaultI18nParams, String language)
693    {
694        if (StringUtils.isNotBlank(_textBodyKeyForInvitation))
695        {
696            return _i18nUtils.translate(_getI18nizableText(_textBodyKeyForInvitation, defaultI18nParams), language);
697        }
698        
699        return null;
700    }
701
702    /**
703     * The email html body for invitation by email
704     * @param defaultI18nParams The default i18n parameters with :
705     * siteName the site name
706     * email the mail
707     * token the token
708     * confirmUri the confirmation uri
709     * siteTitle the site title
710     * siteUrl the site url
711     * @param language the language
712     * @return the email html for invitation by email
713     */
714    public String getHtmlBodyForInvitationEmail(Map<String, I18nizableTextParameter> defaultI18nParams, String language)
715    {
716        if (StringUtils.isNotBlank(_htmlBodyKeyForInvitation))
717        {
718            try
719            {
720                MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
721                        .withLanguage(language)
722                        .withMessage(_getI18nizableText(_htmlBodyKeyForInvitation, defaultI18nParams));
723                    
724                Page signupPage = _getSignupPage(_projectManager.getCatalogSiteName(), language);
725                if (signupPage != null)
726                {
727                    String signupUri = ResolveURIComponent.resolve("page", signupPage.getId(), false, true) + "?email=${email}&token=${token}";
728                    bodyBuilder.withLink(signupUri, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_USER_INVITATION_MAIL_BODY_HTML_LINK_TITLE"));
729                }
730                
731                return bodyBuilder.build();
732            }
733            catch (IOException e)
734            {
735                getLogger().warn("Faild to build HTML body for invitation email", e);
736            }
737        }
738        
739        return null;
740    }
741    
742    /**
743     * The email subject for invitation by email
744     * @param defaultI18nParams The default i18n parameters
745     * @param language the language
746     * @return the email subject for invitation by email
747     */
748    public String getSubjectForInvitationAcceptedEmail (Map<String, I18nizableTextParameter> defaultI18nParams , String language)
749    {
750        if (StringUtils.isNotBlank(_subjectKeyForInvitationAccepted))
751        {
752            return _i18nUtils.translate(_getI18nizableText(_subjectKeyForInvitationAccepted, defaultI18nParams), language);
753        }
754        
755        return null;
756    }
757    
758    /**
759     * The email text body for invitation by email
760     * @param defaultI18nParams The default i18n parameters with :
761     * @param language the language
762     * @return the email text for invitation by email
763     */
764    public String getTextBodyForInvitationAcceptedEmail(Map<String, I18nizableTextParameter> defaultI18nParams, String language)
765    {
766        if (StringUtils.isNotBlank(_textBodyKeyForInvitationAccepted))
767        {
768            return _i18nUtils.translate(_getI18nizableText(_textBodyKeyForInvitationAccepted, defaultI18nParams), language);
769        }
770        
771        return null;
772    }
773    
774    /**
775     * The email html body for invitation by email
776     * @param project The project
777     * @param defaultI18nParams The default i18n parameters with :
778     * @param language the language
779     * @return the email html for invitation by email
780     */
781    public String getHtmlBodyForInvitationAcceptedEmail(Project project, Map<String, I18nizableTextParameter> defaultI18nParams, String language)
782    {
783        if (StringUtils.isNotBlank(_htmlBodyKeyForInvitationAccepted))
784        {
785            try
786            {
787                MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
788                        .withLanguage(language)
789                        .withMessage(_getI18nizableText(_htmlBodyKeyForInvitationAccepted, defaultI18nParams));
790                    
791                Set<Page> memberPages = _projectManager.getModulePages(project, MembersWorkspaceModule.MEMBERS_MODULE_ID);
792                if (!memberPages.isEmpty())
793                {
794                    Page page = memberPages.iterator().next();
795                    String memberPageUri = ResolveURIComponent.resolve("page", page.getId(), false, true);
796                    bodyBuilder.withLink(memberPageUri, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_USER_INVITATION_ACCEPTED_MAIL_BODY_HTML_LINK_TITLE"));
797                }
798                
799                return bodyBuilder.build();
800            }
801            catch (IOException e)
802            {
803                getLogger().warn("Faild to build HTML body for invitation accepted email", e);
804            }
805        }
806        
807        return null;
808    }
809    
810    /**
811     * Get the {@link I18nizableText} from the configured key and i18n parameters
812     * @param fullI18nKey the configured i18n key
813     * @param i18nParams the i18n parameters
814     * @return the i18nizable text
815     */
816    protected I18nizableText _getI18nizableText(String fullI18nKey, Map<String, I18nizableTextParameter> i18nParams)
817    {
818        String catalogue = Strings.CS.contains(fullI18nKey, ":") ? StringUtils.substringBefore(fullI18nKey, ":") : "plugin." + _pluginName;
819        String i18nKey = Strings.CS.contains(fullI18nKey, ":") ? StringUtils.substringAfter(fullI18nKey, ":") : fullI18nKey;
820        
821        return new I18nizableText(catalogue, i18nKey, i18nParams);
822    }
823    
824}