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.collections.CollectionUtils;
036import org.apache.commons.lang.StringUtils;
037import org.apache.excalibur.source.Source;
038import org.apache.excalibur.source.SourceResolver;
039import org.apache.excalibur.source.SourceUtil;
040
041import org.ametys.core.observation.Event;
042import org.ametys.core.observation.Observer;
043import org.ametys.core.right.RightManager;
044import org.ametys.core.user.User;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.user.UserManager;
047import org.ametys.core.util.I18nUtils;
048import org.ametys.core.util.JSONUtils;
049import org.ametys.core.util.mail.SendMailHelper;
050import org.ametys.plugins.explorer.ObservationConstants;
051import org.ametys.plugins.repository.AmetysObject;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper;
054import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper.Frequency;
055import org.ametys.plugins.workspaces.project.objects.Project;
056import org.ametys.runtime.config.Config;
057import org.ametys.runtime.i18n.I18nizableText;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059import org.ametys.runtime.plugin.component.PluginAware;
060import org.ametys.web.WebConstants;
061import org.ametys.web.renderingcontext.RenderingContext;
062import org.ametys.web.renderingcontext.RenderingContextHandler;
063import org.ametys.web.repository.site.Site;
064
065import jakarta.mail.MessagingException;
066
067/**
068 * {@link Observer} for observing events on resources project
069 */
070public abstract class AbstractSendNotificationObserver extends AbstractLogEnabled implements Observer, Serviceable, Contextualizable, PluginAware
071{
072    /** The avalon context */
073    protected Context _context;
074    /** The i18n utils */
075    protected I18nUtils _i18nUtils;
076    /** The JSONUtils */
077    protected JSONUtils _jsonUtils;
078    /** The rendering context handler */
079    protected RenderingContextHandler _renderingContextHandler;
080    /** The Ametys Object Resolver*/
081    protected AmetysObjectResolver _resolver;
082    /** The right manager */
083    protected RightManager _rightManager;
084    /** Source Resolver */
085    protected SourceResolver _srcResolver;
086    /** The users manager */
087    protected UserManager _userManager;
088    /** The notification helper*/
089    protected NotificationPreferencesHelper _notificationPrefHelper;
090
091    /** The name of current plugin */
092    protected String _pluginName;
093    
094    @Override
095    public void service(ServiceManager manager) throws ServiceException
096    {
097        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
098        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
099        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
100        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
101        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
102        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
103        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
104        _notificationPrefHelper = (NotificationPreferencesHelper) manager.lookup(NotificationPreferencesHelper.ROLE);
105    }
106    
107    @Override
108    public void contextualize(Context context) throws ContextException
109    {
110        _context = context;
111    }
112
113    public void setPluginInfo(String pluginName, String featureName, String id)
114    {
115        _pluginName = pluginName;
116    }
117    
118    @Override
119    public int getPriority(Event event)
120    {
121        return Observer.MAX_PRIORITY;
122    }
123    
124    @Override
125    public void observe(Event event, Map<String, Object> transientVars)
126    {
127        Project project = getProject(event);
128        List<UserIdentity> recipients = getUsersToNotify(event.getId(), getEventAmetysObject(event), project);
129        
130        if (project != null && !recipients.isEmpty())
131        {
132            notifyEvent(project, event, recipients);
133        }
134    }
135    
136    /**
137     * Notify email by mail
138     * @param project The project
139     * @param event The event
140     * @param recipients The users to notify
141     */
142    protected void notifyEvent (Project project, Event event, List<UserIdentity> recipients)
143    {
144        Site site = project.getSites().iterator().next();
145        String lang = site.getSitemaps().iterator().next().getName();
146        // Subject
147        I18nizableText i18nSubject = getI18nSubject(event, project);
148        String subject = _i18nUtils.translate(i18nSubject, lang);
149
150        // Body
151        String mailBody;
152        Source source = null;
153        RenderingContext currentContext = _renderingContextHandler.getRenderingContext();
154
155        try
156        {
157            // Force rendering context.FRONT to resolve URI
158            _renderingContextHandler.setRenderingContext(RenderingContext.FRONT);
159            
160            Request request = ContextHelper.getRequest(_context);
161            request.setAttribute("forceAbsoluteUrl", true);
162            
163            request.setAttribute("lang", lang);
164            request.setAttribute(WebConstants.REQUEST_ATTR_SITE, site);
165            request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, site.getName());
166            request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, site.getSkinId());
167            
168            source = _srcResolver.resolveURI(getMailBodyURI(event, project), null, Map.of("event", event, "project", project));
169            
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        sendMail(recipients, subject, mailBody);
193    }
194    
195    /**
196     * Get the AmetysObject that triggered the event to compute the rights
197     * @param event the event
198     * @return the AmetysObject
199     */
200    protected abstract AmetysObject getEventAmetysObject(Event event);
201
202    /**
203     * Returns the URI for HTML mail body
204     * @param event the event
205     * @param project the project
206     * @return The URI for HTML mail body
207     */
208    protected abstract String getMailBodyURI(Event event, Project project);
209
210    /**
211     * Get the {@link I18nizableText} for mail subject
212     * @param event the event
213     * @param project the project
214     * @return the {@link I18nizableText} for subject
215     */
216    protected abstract I18nizableText getI18nSubject(Event event, Project project);
217
218    /**
219     * Get the users allowed to be notified
220     * @param eventId The id of event
221     * @param object The object on which to test rights
222     * @param project The project of the event to test user pref
223     * @return The allowed users
224     */
225    protected List<UserIdentity> getUsersToNotify(String eventId, AmetysObject object, Project project)
226    {
227        boolean returnAll = Config.getInstance().getValue("runtime.mail.massive.sending");
228        Set<UserIdentity> readAccessUsers = _rightManager.getReadAccessAllowedUsers(object).resolveAllowedUsers(returnAll); 
229        Set<UserIdentity> mailAllowedUsers = _rightManager.getAllowedUsers(getRightIdForNotify(), object).resolveAllowedUsers(returnAll); 
230        
231        List<UserIdentity> targets = (List<UserIdentity>) CollectionUtils.retainAll(readAccessUsers, mailAllowedUsers);
232        
233        return targets.stream().filter(userId -> _notificationPrefHelper.askedToBeNotified(userId, project.getName(), Frequency.EACH)).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     * Get the right to check allowed users to notify by mail
307     * @return the right id to check
308     */
309    protected abstract String getRightIdForNotify();
310    
311    /**
312     * format the path without the root path
313     * @param rootPath The root path to remove
314     * @param path The absolute path
315     * @return the local path
316     */
317    protected String _getRelativePath (String rootPath, String path)
318    {
319        int index = path.indexOf(rootPath);
320        return path.substring(index + rootPath.length());
321    }
322}