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}