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}