001/*
002 *  Copyright 2019 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.project.notification;
017
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.context.Context;
025import org.apache.avalon.framework.context.ContextException;
026import org.apache.avalon.framework.context.Contextualizable;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.excalibur.source.Source;
034import org.apache.excalibur.source.SourceResolver;
035import org.apache.excalibur.source.SourceUtil;
036
037import org.ametys.core.group.GroupManager;
038import org.ametys.core.observation.AsyncObserver;
039import org.ametys.core.observation.Event;
040import org.ametys.core.user.UserManager;
041import org.ametys.core.util.I18nUtils;
042import org.ametys.core.util.language.UserLanguagesManager;
043import org.ametys.core.util.mail.SendMailHelper;
044import org.ametys.plugins.repository.AmetysObjectResolver;
045import org.ametys.plugins.workspaces.ObservationConstants;
046import org.ametys.plugins.workspaces.WorkspacesHelper;
047import org.ametys.plugins.workspaces.members.ProjectMemberManager;
048import org.ametys.plugins.workspaces.project.ProjectManager;
049import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper;
050import org.ametys.plugins.workspaces.project.objects.Project;
051import org.ametys.runtime.i18n.I18nizableText;
052import org.ametys.runtime.plugin.component.AbstractLogEnabled;
053import org.ametys.runtime.plugin.component.PluginAware;
054import org.ametys.web.WebConstants;
055import org.ametys.web.population.PopulationContextHelper;
056import org.ametys.web.renderingcontext.RenderingContext;
057import org.ametys.web.renderingcontext.RenderingContextHandler;
058import org.ametys.web.repository.site.Site;
059import org.ametys.web.repository.site.SiteManager;
060
061import jakarta.mail.MessagingException;
062
063/**
064 * Abstract observer for sending mail to members
065 */
066public abstract class AbstractMemberMailNotifierObserver  extends AbstractLogEnabled implements AsyncObserver, PluginAware, Serviceable, Contextualizable
067{
068    /** The avalon context */
069    protected Context _context;
070    /** The name of current plugin */
071    protected String _pluginName;
072    /** The Ametys Object resolver */
073    protected AmetysObjectResolver _resolver;
074    /** The i18n utils */
075    protected I18nUtils _i18nUtils;
076    /** The project member manager */
077    protected ProjectMemberManager _projectMemberManager;
078    /** Project manager */
079    protected ProjectManager _projectManager;
080    /** Site manager */
081    protected SiteManager _siteManager;
082    /** Population context helper */
083    protected PopulationContextHelper _populationContextHelper;
084    /** Source Resolver */
085    protected SourceResolver _srcResolver;
086    /** User manager */
087    protected UserManager _userManager;
088    /** Group manager */
089    protected GroupManager _groupManager;
090    /** The notification preference manager */
091    protected NotificationPreferencesHelper _notificationPrefHelper;
092    /** The rendering context */
093    protected RenderingContextHandler _renderingContextHandler;
094    /** The user languages manager */
095    protected UserLanguagesManager _userLanguagesManager;
096    /** The workspaces helper */
097    protected WorkspacesHelper _workspacesHelper;
098    
099    
100    @Override
101    public void setPluginInfo(String pluginName, String featureName, String id)
102    {
103        _pluginName = pluginName;
104    }
105    
106    @Override
107    public void contextualize(Context context) throws ContextException
108    {
109        _context = context;
110    }
111    
112    public void service(ServiceManager manager) throws ServiceException
113    {
114        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
115        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
116        _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
117        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
118        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
119        _populationContextHelper = (PopulationContextHelper) manager.lookup(org.ametys.core.user.population.PopulationContextHelper.ROLE);
120        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
121        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
122        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
123        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
124        _userLanguagesManager = (UserLanguagesManager) manager.lookup(UserLanguagesManager.ROLE);
125        _workspacesHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE);
126        _notificationPrefHelper = (NotificationPreferencesHelper) manager.lookup(NotificationPreferencesHelper.ROLE);
127    }
128    
129    @Override
130    public int getPriority()
131    {
132        return MIN_PRIORITY;
133    }
134    
135    @Override
136    public void observe(Event event, Map<String, Object> transientVars) throws Exception
137    {
138        Map<String, Object> args = event.getArguments();
139        
140        String projectId = (String) args.get(ObservationConstants.ARGS_PROJECT_ID);
141        Project project = _resolver.resolveById(projectId);
142        Site site = project.getSite();
143        String lang = _workspacesHelper.getLang(project, _userLanguagesManager.getDefaultLanguage());
144        // Compute subject and body
145        I18nizableText i18nSubject = getI18nSubject(event, project);
146        
147        // Send mail to removed members
148        Map<String, List<String>> recipientsByLanguage = getUserToNotifyByLanguage(event, project);
149        
150        for (String language : recipientsByLanguage.keySet())
151        {
152            String userLanguage = StringUtils.defaultIfBlank(language, lang);
153            
154            String subject = _i18nUtils.translate(i18nSubject, userLanguage);
155            
156            String mailBody = null;
157            Source source = null;
158            RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
159            try
160            {
161                // Force rendering context.FRONT to resolve URI
162                _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
163                Request request = ContextHelper.getRequest(_context);
164                request.setAttribute("forceAbsoluteUrl", true);
165                request.setAttribute("lang", userLanguage);
166                request.setAttribute(WebConstants.REQUEST_ATTR_SITE, site);
167                request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, site.getName());
168                request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, site.getSkinId());
169                source = _srcResolver.resolveURI(getMailBodyURI(event, project), null, Map.of("event", event, "project", project));
170                try (InputStream is = source.getInputStream())
171                {
172                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
173                    SourceUtil.copy(is, bos);
174                    
175                    mailBody = bos.toString("UTF-8");
176                }
177            }
178            catch (IOException e)
179            {
180                throw new RuntimeException("Failed to create mail body", e);
181            }
182            finally
183            {
184                _renderingContextHandler.setRenderingContext(currentContext);
185                
186                if (source != null)
187                {
188                    _srcResolver.release(source);
189                }
190            }
191            
192            try
193            {
194                SendMailHelper.newMail()
195                    .withSubject(subject)
196                    .withHTMLBody(mailBody)
197                    .withRecipients(recipientsByLanguage.get(language))
198                    .withAsync(true)
199                    .withInlineCSS(false)
200                    .sendMail();
201            }
202            catch (MessagingException | IOException e)
203            {
204                getLogger().warn("Could not send a notification e-mail to " + recipientsByLanguage + " following his removal from the project " + project.getTitle(), e);
205            }
206        }
207    }
208    
209    /**
210     * Get recipients' emails sorted by language
211     * @param event the event
212     * @param project the project
213     * @return the recipients' emails sorted by language
214     */
215    protected abstract Map<String, List<String>> getUserToNotifyByLanguage(Event event, Project project);
216    
217    /**
218     * Returns the URI for HTML mail body
219     * @param event the event
220     * @param project the project
221     * @return The URI for HTML mail body
222     */
223    protected String getMailBodyURI(Event event, Project project)
224    {
225        return "cocoon://_plugins/workspaces/notification-mail-member";
226    }
227    
228    /**
229     * Get the {@link I18nizableText} for mail subject
230     * @param event the event
231     * @param project the project
232     * @return the {@link I18nizableText} for subject
233     */
234    protected abstract I18nizableText getI18nSubject(Event event, Project project);
235}