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 eventInfo.put(NotificationConstants.SERVER_URL_PREFIX, notificationService.getServerUrlPrefix()); 244 245 // Get notification document codec 246 DocumentViewCodecManager codecService = Framework.getService(DocumentViewCodecManager.class); 247 DocumentViewCodec codec = codecService.getCodec(NOTIFICATION_DOCUMENT_ID_CODEC_NAME); 248 boolean isNotificationCodec = codec != null; 249 boolean isJSFUI = isNotificationCodec && JSF_NOTIFICATION_DOCUMENT_ID_CODEC_PREFIX.equals(codec.getPrefix()); 250 251 eventInfo.put(NotificationConstants.IS_JSF_UI, isJSFUI); 252 eventInfo.put(NotificationConstants.DESTINATION_KEY, subscriptor); 253 eventInfo.put(NotificationConstants.NOTIFICATION_KEY, notification); 254 eventInfo.put(NotificationConstants.DOCUMENT_ID_KEY, doc.getId()); 255 eventInfo.put(NotificationConstants.DATE_TIME_KEY, new Date(event.getTime())); 256 eventInfo.put(NotificationConstants.AUTHOR_KEY, author); 257 eventInfo.put(NotificationConstants.DOCUMENT_VERSION, doc.getVersionLabel()); 258 eventInfo.put(NotificationConstants.DOCUMENT_STATE, doc.getCurrentLifeCycleState()); 259 eventInfo.put(NotificationConstants.DOCUMENT_CREATED, created.getTime()); 260 if (isNotificationCodec) { 261 StringBuilder userUrl = new StringBuilder(); 262 userUrl.append(notificationService.getServerUrlPrefix()); 263 if (!isJSFUI) { 264 userUrl.append("ui/"); 265 userUrl.append("#!/"); 266 } 267 userUrl.append("user/").append(ctx.getPrincipal().getName()); 268 eventInfo.put(NotificationConstants.USER_URL_KEY, userUrl.toString()); 269 } 270 eventInfo.put(NotificationConstants.DOCUMENT_LOCATION, doc.getPathAsString()); 271 // Main file link for downloading 272 BlobHolder bh = doc.getAdapter(BlobHolder.class); 273 if (bh != null && bh.getBlob() != null) { 274 DownloadService downloadService = Framework.getService(DownloadService.class); 275 String filename = bh.getBlob().getFilename(); 276 String docMainFile = notificationService.getServerUrlPrefix() 277 + downloadService.getDownloadUrl(doc, DownloadService.BLOBHOLDER_0, filename); 278 eventInfo.put(NotificationConstants.DOCUMENT_MAIN_FILE, docMainFile); 279 } 280 281 if (!isDeleteEvent(event.getName())) { 282 if (isNotificationCodec) { 283 eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY, 284 codecService.getUrlFromDocumentView(NOTIFICATION_DOCUMENT_ID_CODEC_NAME, 285 new DocumentViewImpl(doc), true, notificationService.getServerUrlPrefix())); 286 } 287 eventInfo.put(NotificationConstants.DOCUMENT_TITLE_KEY, doc.getTitle()); 288 } 289 290 if (isInterestedInNotification(notification)) { 291 sendNotification(event, ctx); 292 if (log.isDebugEnabled()) { 293 log.debug("notification " + notification.getName() + " sent to " + notification.getSubject()); 294 } 295 } 296 } 297 298 public void sendNotification(Event event, DocumentEventContext ctx) { 299 300 String eventId = event.getName(); 301 log.debug("Received a message for notification sender with eventId : " + eventId); 302 303 Map<String, Serializable> eventInfo = ctx.getProperties(); 304 String userDest = (String) eventInfo.get(NotificationConstants.DESTINATION_KEY); 305 NotificationImpl notif = (NotificationImpl) eventInfo.get(NotificationConstants.NOTIFICATION_KEY); 306 307 // send email 308 NuxeoPrincipal recepient = NotificationServiceHelper.getUsersService().getPrincipal(userDest); 309 if (recepient == null) { 310 log.error("Couldn't find user: " + userDest + " to send her a mail."); 311 return; 312 } 313 String email = recepient.getEmail(); 314 if (email == null || "".equals(email)) { 315 log.error("No email found for user: " + userDest); 316 return; 317 } 318 319 String subjectTemplate = notif.getSubjectTemplate(); 320 321 String mailTemplate = null; 322 // mail template can be dynamically computed from a MVEL expression 323 if (notif.getTemplateExpr() != null) { 324 try { 325 mailTemplate = emailHelper.evaluateMvelExpresssion(notif.getTemplateExpr(), eventInfo); 326 } catch (PropertyAccessException pae) { 327 if (log.isDebugEnabled()) { 328 log.debug("Cannot evaluate mail template expression '" + notif.getTemplateExpr() 329 + "' in that context " + eventInfo, pae); 330 } 331 } 332 } 333 // if there is no mailTemplate evaluated, use the defined one 334 if (StringUtils.isEmpty(mailTemplate)) { 335 mailTemplate = notif.getTemplate(); 336 } 337 338 log.debug("email: " + email); 339 log.debug("mail template: " + mailTemplate); 340 log.debug("subject template: " + subjectTemplate); 341 342 Map<String, Object> mail = new HashMap<>(); 343 mail.put("mail.to", email); 344 345 String authorUsername = (String) eventInfo.get(NotificationConstants.AUTHOR_KEY); 346 347 if (authorUsername != null) { 348 NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername); 349 mail.put(NotificationConstants.PRINCIPAL_AUTHOR_KEY, author); 350 } 351 352 mail.put(NotificationConstants.DOCUMENT_KEY, ctx.getSourceDocument()); 353 String subject = notif.getSubject() == null ? NotificationConstants.NOTIFICATION_KEY : notif.getSubject(); 354 subject = notificationService.getEMailSubjectPrefix() + subject; 355 mail.put("subject", subject); 356 mail.put("template", mailTemplate); 357 mail.put("subjectTemplate", subjectTemplate); 358 359 // Transferring all data from event to email 360 for (String key : eventInfo.keySet()) { 361 mail.put(key, eventInfo.get(key) == null ? "" : eventInfo.get(key)); 362 log.debug("Mail prop: " + key); 363 } 364 365 mail.put(NotificationConstants.EVENT_ID_KEY, eventId); 366 367 try { 368 emailHelper.sendmail(mail); 369 } catch (MessagingException e) { 370 String cause = ""; 371 if ((e instanceof SendFailedException) && (e.getCause() instanceof SendFailedException)) { 372 cause = " - Cause: " + e.getCause().getMessage(); 373 } 374 log.warn("Failed to send notification email to '" + email + "': " + e.getClass().getName() + ": " 375 + e.getMessage() + cause); 376 } 377 } 378 379 /** 380 * Adds the concerned users to the list of targeted users for these notifications. 381 */ 382 private void gatherConcernedUsersForDocument(CoreSession coreSession, DocumentModel doc, List<Notification> notifs, 383 Map<Notification, List<String>> targetUsers) { 384 if (doc.getPath().segmentCount() > 1) { 385 log.debug("Searching document: " + doc.getName()); 386 getInterstedUsers(doc, notifs, targetUsers); 387 if (doc.getParentRef() != null && coreSession.exists(doc.getParentRef())) { 388 DocumentModel parent = getDocumentParent(coreSession, doc); 389 gatherConcernedUsersForDocument(coreSession, parent, notifs, targetUsers); 390 } 391 } 392 } 393 394 private DocumentModel getDocumentParent(CoreSession coreSession, DocumentModel doc) { 395 if (doc == null) { 396 return null; 397 } 398 return coreSession.getDocument(doc.getParentRef()); 399 } 400 401 private void getInterstedUsers(DocumentModel doc, List<Notification> notifs, 402 Map<Notification, List<String>> targetUsers) { 403 for (Notification notification : notifs) { 404 if (!notification.getAutoSubscribed()) { 405 List<String> userGroup = notificationService.getSubscribers(notification.getName(), doc); 406 for (String subscriptor : userGroup) { 407 if (subscriptor != null) { 408 if (isUser(subscriptor)) { 409 storeUserForNotification(notification, subscriptor.substring(5), targetUsers); 410 } else { 411 // it is a group - get all users and send 412 // notifications to them 413 List<String> usersOfGroup = getGroupMembers(subscriptor.substring(6)); 414 if (usersOfGroup != null && !usersOfGroup.isEmpty()) { 415 for (String usr : usersOfGroup) { 416 storeUserForNotification(notification, usr, targetUsers); 417 } 418 } 419 } 420 } 421 } 422 } else { 423 // An automatic notification happens 424 // should be sent to interested users 425 targetUsers.put(notification, new ArrayList<>()); 426 } 427 } 428 } 429 430 private static void storeUserForNotification(Notification notification, String user, 431 Map<Notification, List<String>> targetUsers) { 432 List<String> subscribedUsers = targetUsers.computeIfAbsent(notification, k -> new ArrayList<>()); 433 if (!subscribedUsers.contains(user)) { 434 subscribedUsers.add(user); 435 } 436 } 437 438 private boolean isDeleteEvent(String eventId) { 439 List<String> deletionEvents = new ArrayList<>(); 440 deletionEvents.add("aboutToRemove"); 441 deletionEvents.add("documentRemoved"); 442 return deletionEvents.contains(eventId); 443 } 444 445 private boolean isUser(String subscriptor) { 446 return subscriptor != null && subscriptor.startsWith("user:"); 447 } 448 449 public boolean isInterestedInNotification(Notification notif) { 450 return notif != null && "email".equals(notif.getChannel()); 451 } 452 453 public EmailHelper getEmailHelper() { 454 return emailHelper; 455 } 456 457 public void setEmailHelper(EmailHelper emailHelper) { 458 this.emailHelper = emailHelper; 459 } 460 461}