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}