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}