001/*
002 *  Copyright 2010 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;
023import java.util.Objects;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.commons.lang.StringUtils;
036import org.apache.excalibur.source.Source;
037import org.apache.excalibur.source.SourceResolver;
038import org.apache.excalibur.source.SourceUtil;
039
040import org.ametys.core.observation.Event;
041import org.ametys.core.observation.Observer;
042import org.ametys.core.right.RightManager;
043import org.ametys.core.user.User;
044import org.ametys.core.user.UserIdentity;
045import org.ametys.core.user.UserManager;
046import org.ametys.core.util.I18nUtils;
047import org.ametys.core.util.JSONUtils;
048import org.ametys.core.util.mail.SendMailHelper;
049import org.ametys.plugins.explorer.ObservationConstants;
050import org.ametys.plugins.repository.AmetysObject;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper;
053import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper.Frequency;
054import org.ametys.plugins.workspaces.project.objects.Project;
055import org.ametys.runtime.config.Config;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058import org.ametys.runtime.plugin.component.PluginAware;
059import org.ametys.web.WebConstants;
060import org.ametys.web.renderingcontext.RenderingContext;
061import org.ametys.web.renderingcontext.RenderingContextHandler;
062import org.ametys.web.repository.site.Site;
063
064import jakarta.mail.MessagingException;
065
066/**
067 * {@link Observer} for observing events on resources project
068 */
069public abstract class AbstractSendNotificationObserver extends AbstractLogEnabled implements Observer, Serviceable, Contextualizable, PluginAware
070{
071    /** The avalon context */
072    protected Context _context;
073    /** The i18n utils */
074    protected I18nUtils _i18nUtils;
075    /** The JSONUtils */
076    protected JSONUtils _jsonUtils;
077    /** The rendering context handler */
078    protected RenderingContextHandler _renderingContextHandler;
079    /** The Ametys Object Resolver*/
080    protected AmetysObjectResolver _resolver;
081    /** The right manager */
082    protected RightManager _rightManager;
083    /** Source Resolver */
084    protected SourceResolver _srcResolver;
085    /** The users manager */
086    protected UserManager _userManager;
087    /** The notification helper*/
088    protected NotificationPreferencesHelper _notificationPrefHelper;
089
090    /** The name of current plugin */
091    protected String _pluginName;
092    
093    @Override
094    public void service(ServiceManager manager) throws ServiceException
095    {
096        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
097        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
098        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
099        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
100        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
101        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
102        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
103        _notificationPrefHelper = (NotificationPreferencesHelper) manager.lookup(NotificationPreferencesHelper.ROLE);
104    }
105    
106    @Override
107    public void contextualize(Context context) throws ContextException
108    {
109        _context = context;
110    }
111
112    public void setPluginInfo(String pluginName, String featureName, String id)
113    {
114        _pluginName = pluginName;
115    }
116    
117    @Override
118    public int getPriority(Event event)
119    {
120        return Observer.MAX_PRIORITY;
121    }
122    
123    @Override
124    public void observe(Event event, Map<String, Object> transientVars)
125    {
126        Project project = getProject(event);
127        if (project != null)
128        {
129            List<UserIdentity> recipients = getUsersToNotify(event.getId(), getEventAmetysObject(event), project);
130            if (!recipients.isEmpty())
131            {
132                notifyEvent(project, event, recipients);
133            }
134        }
135    }
136    
137    /**
138     * Notify email by mail
139     * @param project The project
140     * @param event The event
141     * @param recipients The users to notify
142     */
143    protected void notifyEvent (Project project, Event event, List<UserIdentity> recipients)
144    {
145        Site site = project.getSite();
146        String lang = site.getSitemaps().iterator().next().getName();
147        // Subject
148        I18nizableText i18nSubject = getI18nSubject(event, project);
149        String subject = _i18nUtils.translate(i18nSubject, lang);
150
151        // Body
152        String mailBody;
153        Source source = null;
154        RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
155
156        try
157        {
158            // Force rendering context.FRONT to resolve URI
159            _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
160            
161            Request request = ContextHelper.getRequest(_context);
162            request.setAttribute("forceAbsoluteUrl", true);
163            
164            request.setAttribute("lang", lang);
165            request.setAttribute(WebConstants.REQUEST_ATTR_SITE, site);
166            request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, site.getName());
167            request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, site.getSkinId());
168            
169            source = _srcResolver.resolveURI(getMailBodyURI(event, project), null, Map.of("event", event, "project", project));
170            
171            try (InputStream is = source.getInputStream())
172            {
173                ByteArrayOutputStream bos = new ByteArrayOutputStream();
174                SourceUtil.copy(is, bos);
175                
176                mailBody = bos.toString("UTF-8");
177            }
178        }
179        catch (IOException e)
180        {
181            throw new RuntimeException("Failed to create mail body", e);
182        }
183        finally
184        {
185            _renderingContextHandler.setRenderingContext(currentContext);
186            
187            if (source != null)
188            {
189                _srcResolver.release(source);
190            }
191        }
192        
193        sendMail(recipients, subject, mailBody);
194    }
195    
196    /**
197     * Get the AmetysObject that triggered the event to compute the rights
198     * @param event the event
199     * @return the AmetysObject
200     */
201    protected abstract AmetysObject getEventAmetysObject(Event event);
202
203    /**
204     * Returns the URI for HTML mail body
205     * @param event the event
206     * @param project the project
207     * @return The URI for HTML mail body
208     */
209    protected abstract String getMailBodyURI(Event event, Project project);
210
211    /**
212     * Get the {@link I18nizableText} for mail subject
213     * @param event the event
214     * @param project the project
215     * @return the {@link I18nizableText} for subject
216     */
217    protected abstract I18nizableText getI18nSubject(Event event, Project project);
218
219    /**
220     * Get the users allowed to be notified
221     * @param eventId The id of event
222     * @param object The object on which to test rights
223     * @param project The project of the event to test user pref
224     * @return The allowed users
225     */
226    protected List<UserIdentity> getUsersToNotify(String eventId, AmetysObject object, Project project)
227    {
228        boolean returnAll = Config.getInstance().getValue("runtime.mail.massive.sending");
229        Set<UserIdentity> readAccessUsers = _rightManager.getReadAccessAllowedUsers(object).resolveAllowedUsers(returnAll); 
230        
231        return readAccessUsers.stream()
232                .filter(userId -> _notificationPrefHelper.askedToBeNotified(userId, project.getName(), Frequency.EACH))
233                .collect(Collectors.toList());
234    }
235    
236    /**
237     * Get the project from event
238     * @param event The event
239     * @return the project or null if not found
240     */
241    protected Project getProject (Event event)
242    {
243        Map<String, Object> args = event.getArguments();
244        
245        String targetId = (String) args.get(ObservationConstants.ARGS_ID);
246        String parentID = (String) args.get(ObservationConstants.ARGS_PARENT_ID);
247        
248        AmetysObject object = null;
249        if (parentID != null)
250        {
251            object = _resolver.resolveById(parentID);
252        }
253        else
254        {
255            object = _resolver.resolveById(targetId);
256        }
257        
258        AmetysObject parent = object.getParent();
259        
260        while (parent != null)
261        {
262            if (parent instanceof Project)
263            {
264                return (Project) parent;
265            }
266            
267            parent = parent.getParent();
268        }
269        
270        return null;
271    }
272    
273    
274    /**
275     * Sent an email
276     * @param recipients The recipients of the mail 
277     * @param subject The subject of the mail
278     * @param htmlMailBody The HTML mail body
279     */
280    protected void sendMail(List<UserIdentity> recipients, String subject, String htmlMailBody)
281    {
282        List<String> recipientAdresses = recipients.stream()
283                                                   .map(_userManager::getUser)
284                                                   .filter(Objects::nonNull)
285                                                   .map(User::getEmail)
286                                                   .filter(StringUtils::isNotEmpty)
287                                                   .collect(Collectors.toList());
288            
289        try
290        {
291            SendMailHelper.newMail()
292                          .withSubject(subject)
293                          .withHTMLBody(htmlMailBody)
294                          .withRecipients(recipientAdresses)
295                          .withAsync(true)
296                          .withInlineCSS(false)
297                          .sendMail();
298        }
299        catch (MessagingException | IOException e)
300        {
301            getLogger().warn("Could not send a notification e-mail to " + recipientAdresses, e);
302        }
303    }
304    
305    /**
306     * format the path without the root path
307     * @param rootPath The root path to remove
308     * @param path The absolute path
309     * @return the local path
310     */
311    protected String _getRelativePath (String rootPath, String path)
312    {
313        int index = path.indexOf(rootPath);
314        return path.substring(index + rootPath.length());
315    }
316}