001/* 002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 * Vladimir Pasquier <vpasquier@nuxeo.com> 019 * 020 */ 021 022package org.nuxeo.ecm.platform.ec.notification; 023 024import java.io.Serializable; 025import java.util.ArrayList; 026import java.util.Calendar; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033 034import javax.mail.MessagingException; 035import javax.mail.SendFailedException; 036 037import org.apache.commons.lang3.StringUtils; 038import org.apache.commons.logging.Log; 039import org.apache.commons.logging.LogFactory; 040import org.mvel2.PropertyAccessException; 041import org.nuxeo.ecm.core.api.CoreSession; 042import org.nuxeo.ecm.core.api.DocumentModel; 043import org.nuxeo.ecm.core.api.NuxeoGroup; 044import org.nuxeo.ecm.core.api.NuxeoPrincipal; 045import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 046import org.nuxeo.ecm.core.api.security.SecurityConstants; 047import org.nuxeo.ecm.core.event.Event; 048import org.nuxeo.ecm.core.event.EventBundle; 049import org.nuxeo.ecm.core.event.EventContext; 050import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener; 051import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 052import org.nuxeo.ecm.core.event.impl.ShallowDocumentModel; 053import org.nuxeo.ecm.core.io.download.DownloadService; 054import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper; 055import org.nuxeo.ecm.platform.ec.notification.service.NotificationService; 056import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper; 057import org.nuxeo.ecm.platform.notification.api.Notification; 058import org.nuxeo.ecm.platform.url.DocumentViewImpl; 059import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager; 060import org.nuxeo.ecm.platform.url.codec.api.DocumentViewCodec; 061import org.nuxeo.ecm.platform.usermanager.UserManager; 062import org.nuxeo.runtime.api.Framework; 063 064public class NotificationEventListener implements PostCommitFilteringEventListener { 065 066 private static final Log log = LogFactory.getLog(NotificationEventListener.class); 067 068 private static final String CHECK_READ_PERMISSION_PROPERTY = "notification.check.read.permission"; 069 070 public static final String NOTIFICATION_DOCUMENT_ID_CODEC_NAME = "notificationDocId"; 071 072 public static final String JSF_NOTIFICATION_DOCUMENT_ID_CODEC_PREFIX = "nxdoc"; 073 074 private UserManager userManager; 075 076 private EmailHelper emailHelper = new EmailHelper(); 077 078 private NotificationService notificationService = NotificationServiceHelper.getNotificationService(); 079 080 @Override 081 public boolean acceptEvent(Event event) { 082 if (notificationService == null) { 083 return false; 084 } 085 return notificationService.getNotificationEventNames().contains(event.getName()); 086 } 087 088 @Override 089 public void handleEvent(EventBundle events) { 090 091 if (notificationService == null) { 092 log.error("Unable to get NotificationService, exiting"); 093 return; 094 } 095 boolean processEvents = false; 096 for (String name : notificationService.getNotificationEventNames()) { 097 if (events.containsEventName(name)) { 098 processEvents = true; 099 break; 100 } 101 } 102 if (!processEvents) { 103 return; 104 } 105 for (Event event : events) { 106 Boolean block = (Boolean) event.getContext() 107 .getProperty(NotificationConstants.DISABLE_NOTIFICATION_SERVICE); 108 if (block != null && block) { 109 // ignore the event - we are blocked by the caller 110 continue; 111 } 112 List<Notification> notifs = notificationService.getNotificationsForEvents(event.getName()); 113 if (notifs != null && !notifs.isEmpty()) { 114 handleNotifications(event, notifs); 115 } 116 } 117 118 } 119 120 protected void handleNotifications(Event event, List<Notification> notifs) { 121 122 EventContext ctx = event.getContext(); 123 DocumentEventContext docCtx; 124 if (ctx instanceof DocumentEventContext) { 125 docCtx = (DocumentEventContext) ctx; 126 } else { 127 log.warn("Can not handle notification on a event that is not bound to a DocumentEventContext"); 128 return; 129 } 130 131 if (docCtx.getSourceDocument() instanceof ShallowDocumentModel) { 132 log.trace("Can not handle notification on a event that is bound to a ShallowDocument"); 133 return; 134 } 135 136 CoreSession coreSession = event.getContext().getCoreSession(); 137 Map<String, Serializable> properties = event.getContext().getProperties(); 138 Map<Notification, List<String>> targetUsers = new HashMap<>(); 139 140 for (NotificationListenerVeto veto : notificationService.getNotificationVetos()) { 141 if (!veto.accept(event)) { 142 return; 143 } 144 } 145 146 for (NotificationListenerHook hookListener : notificationService.getListenerHooks()) { 147 hookListener.handleNotifications(event); 148 } 149 150 gatherConcernedUsersForDocument(coreSession, docCtx.getSourceDocument(), notifs, targetUsers); 151 152 for (Notification notif : targetUsers.keySet()) { 153 if (!notif.getAutoSubscribed()) { 154 for (String user : targetUsers.get(notif)) { 155 sendNotificationSignalForUser(notif, user, event, docCtx); 156 } 157 } else { 158 Object recipientProperty = properties.get(NotificationConstants.RECIPIENTS_KEY); 159 String[] recipients = null; 160 if (recipientProperty != null) { 161 if (recipientProperty instanceof String[]) { 162 recipients = (String[]) properties.get(NotificationConstants.RECIPIENTS_KEY); 163 } else if (recipientProperty instanceof String) { 164 recipients = new String[1]; 165 recipients[0] = (String) recipientProperty; 166 } 167 } 168 if (recipients == null) { 169 continue; 170 } 171 Set<String> users = new HashSet<>(); 172 for (String recipient : recipients) { 173 if (recipient == null) { 174 continue; 175 } 176 if (recipient.contains(NuxeoPrincipal.PREFIX)) { 177 users.add(recipient.replace(NuxeoPrincipal.PREFIX, "")); 178 } else if (recipient.contains(NuxeoGroup.PREFIX)) { 179 List<String> groupMembers = getGroupMembers(recipient.replace(NuxeoGroup.PREFIX, "")); 180 for (String member : groupMembers) { 181 users.add(member); 182 } 183 } else { 184 // test if the unprefixed recipient corresponds to a 185 // group, to fetch its members 186 if (NotificationServiceHelper.getUsersService().getGroup(recipient) != null) { 187 users.addAll(getGroupMembers(recipient)); 188 } else { 189 users.add(recipient); 190 } 191 } 192 193 } 194 for (String user : users) { 195 sendNotificationSignalForUser(notif, user, event, docCtx); 196 } 197 198 } 199 } 200 201 } 202 203 protected UserManager getUserManager() { 204 if (userManager == null) { 205 userManager = Framework.getService(UserManager.class); 206 } 207 return userManager; 208 } 209 210 protected List<String> getGroupMembers(String groupId) { 211 return getUserManager().getUsersInGroupAndSubGroups(groupId); 212 } 213 214 protected void sendNotificationSignalForUser(Notification notification, String subscriptor, Event event, 215 DocumentEventContext ctx) { 216 217 if (SecurityConstants.SYSTEM_USERNAME.equals(subscriptor)) { 218 // it doesn't make sense to notify the system user 219 return; 220 } 221 NuxeoPrincipal principal = getUserManager().getPrincipal(subscriptor); 222 if (principal == null) { 223 log.error("No Nuxeo principal found for '" + subscriptor 224 + "'. No notification will be sent to this user"); 225 return; 226 } 227 228 if (Boolean.parseBoolean(Framework.getProperty(CHECK_READ_PERMISSION_PROPERTY))) { 229 if (!ctx.getCoreSession().hasPermission(principal, ctx.getSourceDocument().getRef(), 230 SecurityConstants.READ)) { 231 log.debug("Notification will not be sent: + '" + subscriptor 232 + "' do not have Read permission on document " + ctx.getSourceDocument().getId()); 233 return; 234 } 235 } 236 237 log.debug("Producing notification message."); 238 239 Map<String, Serializable> eventInfo = ctx.getProperties(); 240 DocumentModel doc = ctx.getSourceDocument(); 241 String author = ctx.getPrincipal().getName(); 242 Calendar created = (Calendar) ctx.getSourceDocument().getPropertyValue("dc:created"); 243 244 // Get notification document codec 245 DocumentViewCodecManager codecService = Framework.getService(DocumentViewCodecManager.class); 246 DocumentViewCodec codec = codecService.getCodec(NOTIFICATION_DOCUMENT_ID_CODEC_NAME); 247 boolean isNotificationCodec = codec != null; 248 boolean isJSFUI = isNotificationCodec && JSF_NOTIFICATION_DOCUMENT_ID_CODEC_PREFIX.equals(codec.getPrefix()); 249 250 eventInfo.put(NotificationConstants.IS_JSF_UI, isJSFUI); 251 eventInfo.put(NotificationConstants.DESTINATION_KEY, subscriptor); 252 eventInfo.put(NotificationConstants.NOTIFICATION_KEY, notification); 253 eventInfo.put(NotificationConstants.DOCUMENT_ID_KEY, doc.getId()); 254 eventInfo.put(NotificationConstants.DATE_TIME_KEY, new Date(event.getTime())); 255 eventInfo.put(NotificationConstants.AUTHOR_KEY, author); 256 eventInfo.put(NotificationConstants.DOCUMENT_VERSION, doc.getVersionLabel()); 257 eventInfo.put(NotificationConstants.DOCUMENT_STATE, doc.getCurrentLifeCycleState()); 258 eventInfo.put(NotificationConstants.DOCUMENT_CREATED, created.getTime()); 259 if (isNotificationCodec) { 260 StringBuilder userUrl = new StringBuilder(); 261 userUrl.append(notificationService.getServerUrlPrefix()); 262 if (!isJSFUI) { 263 userUrl.append("ui/"); 264 userUrl.append("#!/"); 265 } 266 userUrl.append("user/").append(ctx.getPrincipal().getName()); 267 eventInfo.put(NotificationConstants.USER_URL_KEY, userUrl.toString()); 268 } 269 eventInfo.put(NotificationConstants.DOCUMENT_LOCATION, doc.getPathAsString()); 270 // Main file link for downloading 271 BlobHolder bh = doc.getAdapter(BlobHolder.class); 272 if (bh != null && bh.getBlob() != null) { 273 DownloadService downloadService = Framework.getService(DownloadService.class); 274 String filename = bh.getBlob().getFilename(); 275 String docMainFile = notificationService.getServerUrlPrefix() 276 + downloadService.getDownloadUrl(doc, DownloadService.BLOBHOLDER_0, filename); 277 eventInfo.put(NotificationConstants.DOCUMENT_MAIN_FILE, docMainFile); 278 } 279 280 if (!isDeleteEvent(event.getName())) { 281 if (isNotificationCodec) { 282 eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY, 283 codecService.getUrlFromDocumentView(NOTIFICATION_DOCUMENT_ID_CODEC_NAME, 284 new DocumentViewImpl(doc), true, notificationService.getServerUrlPrefix())); 285 } 286 eventInfo.put(NotificationConstants.DOCUMENT_TITLE_KEY, doc.getTitle()); 287 } 288 289 if (isInterestedInNotification(notification)) { 290 sendNotification(event, ctx); 291 if (log.isDebugEnabled()) { 292 log.debug("notification " + notification.getName() + " sent to " + notification.getSubject()); 293 } 294 } 295 } 296 297 public void sendNotification(Event event, DocumentEventContext ctx) { 298 299 String eventId = event.getName(); 300 log.debug("Received a message for notification sender with eventId : " + eventId); 301 302 Map<String, Serializable> eventInfo = ctx.getProperties(); 303 String userDest = (String) eventInfo.get(NotificationConstants.DESTINATION_KEY); 304 NotificationImpl notif = (NotificationImpl) eventInfo.get(NotificationConstants.NOTIFICATION_KEY); 305 306 // send email 307 NuxeoPrincipal recepient = NotificationServiceHelper.getUsersService().getPrincipal(userDest); 308 if (recepient == null) { 309 log.error("Couldn't find user: " + userDest + " to send her a mail."); 310 return; 311 } 312 String email = recepient.getEmail(); 313 if (email == null || "".equals(email)) { 314 log.error("No email found for user: " + userDest); 315 return; 316 } 317 318 String subjectTemplate = notif.getSubjectTemplate(); 319 320 String mailTemplate = null; 321 // mail template can be dynamically computed from a MVEL expression 322 if (notif.getTemplateExpr() != null) { 323 try { 324 mailTemplate = emailHelper.evaluateMvelExpresssion(notif.getTemplateExpr(), eventInfo); 325 } catch (PropertyAccessException pae) { 326 if (log.isDebugEnabled()) { 327 log.debug("Cannot evaluate mail template expression '" + notif.getTemplateExpr() 328 + "' in that context " + eventInfo, pae); 329 } 330 } 331 } 332 // if there is no mailTemplate evaluated, use the defined one 333 if (StringUtils.isEmpty(mailTemplate)) { 334 mailTemplate = notif.getTemplate(); 335 } 336 337 log.debug("email: " + email); 338 log.debug("mail template: " + mailTemplate); 339 log.debug("subject template: " + subjectTemplate); 340 341 Map<String, Object> mail = new HashMap<>(); 342 mail.put("mail.to", email); 343 344 String authorUsername = (String) eventInfo.get(NotificationConstants.AUTHOR_KEY); 345 346 if (authorUsername != null) { 347 NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername); 348 mail.put(NotificationConstants.PRINCIPAL_AUTHOR_KEY, author); 349 } 350 351 mail.put(NotificationConstants.DOCUMENT_KEY, ctx.getSourceDocument()); 352 String subject = notif.getSubject() == null ? NotificationConstants.NOTIFICATION_KEY : notif.getSubject(); 353 subject = notificationService.getEMailSubjectPrefix() + subject; 354 mail.put("subject", subject); 355 mail.put("template", mailTemplate); 356 mail.put("subjectTemplate", subjectTemplate); 357 358 // Transferring all data from event to email 359 for (String key : eventInfo.keySet()) { 360 mail.put(key, eventInfo.get(key) == null ? "" : eventInfo.get(key)); 361 log.debug("Mail prop: " + key); 362 } 363 364 mail.put(NotificationConstants.EVENT_ID_KEY, eventId); 365 366 try { 367 emailHelper.sendmail(mail); 368 } catch (MessagingException e) { 369 String cause = ""; 370 if ((e instanceof SendFailedException) && (e.getCause() instanceof SendFailedException)) { 371 cause = " - Cause: " + e.getCause().getMessage(); 372 } 373 log.warn("Failed to send notification email to '" + email + "': " + e.getClass().getName() + ": " 374 + e.getMessage() + cause); 375 } 376 } 377 378 /** 379 * Adds the concerned users to the list of targeted users for these notifications. 380 */ 381 private void gatherConcernedUsersForDocument(CoreSession coreSession, DocumentModel doc, List<Notification> notifs, 382 Map<Notification, List<String>> targetUsers) { 383 if (doc.getPath().segmentCount() > 1) { 384 log.debug("Searching document: " + doc.getName()); 385 getInterstedUsers(doc, notifs, targetUsers); 386 if (doc.getParentRef() != null && coreSession.exists(doc.getParentRef())) { 387 DocumentModel parent = getDocumentParent(coreSession, doc); 388 gatherConcernedUsersForDocument(coreSession, parent, notifs, targetUsers); 389 } 390 } 391 } 392 393 private DocumentModel getDocumentParent(CoreSession coreSession, DocumentModel doc) { 394 if (doc == null) { 395 return null; 396 } 397 return coreSession.getDocument(doc.getParentRef()); 398 } 399 400 private void getInterstedUsers(DocumentModel doc, List<Notification> notifs, 401 Map<Notification, List<String>> targetUsers) { 402 for (Notification notification : notifs) { 403 if (!notification.getAutoSubscribed()) { 404 List<String> userGroup = notificationService.getSubscribers(notification.getName(), doc); 405 for (String subscriptor : userGroup) { 406 if (subscriptor != null) { 407 if (isUser(subscriptor)) { 408 storeUserForNotification(notification, subscriptor.substring(5), targetUsers); 409 } else { 410 // it is a group - get all users and send 411 // notifications to them 412 List<String> usersOfGroup = getGroupMembers(subscriptor.substring(6)); 413 if (usersOfGroup != null && !usersOfGroup.isEmpty()) { 414 for (String usr : usersOfGroup) { 415 storeUserForNotification(notification, usr, targetUsers); 416 } 417 } 418 } 419 } 420 } 421 } else { 422 // An automatic notification happens 423 // should be sent to interested users 424 targetUsers.put(notification, new ArrayList<>()); 425 } 426 } 427 } 428 429 private static void storeUserForNotification(Notification notification, String user, 430 Map<Notification, List<String>> targetUsers) { 431 List<String> subscribedUsers = targetUsers.computeIfAbsent(notification, k -> new ArrayList<>()); 432 if (!subscribedUsers.contains(user)) { 433 subscribedUsers.add(user); 434 } 435 } 436 437 private boolean isDeleteEvent(String eventId) { 438 List<String> deletionEvents = new ArrayList<>(); 439 deletionEvents.add("aboutToRemove"); 440 deletionEvents.add("documentRemoved"); 441 return deletionEvents.contains(eventId); 442 } 443 444 private boolean isUser(String subscriptor) { 445 return subscriptor != null && subscriptor.startsWith("user:"); 446 } 447 448 public boolean isInterestedInNotification(Notification notif) { 449 return notif != null && "email".equals(notif.getChannel()); 450 } 451 452 public EmailHelper getEmailHelper() { 453 return emailHelper; 454 } 455 456 public void setEmailHelper(EmailHelper emailHelper) { 457 this.emailHelper = emailHelper; 458 } 459 460}