001/*
002 * (C) Copyright 2015 Nuxeo SA (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 *     Thomas Roger
018 */
019
020package org.nuxeo.ecm.permissions;
021
022import static org.nuxeo.ecm.permissions.Constants.ACE_GRANTED_TEMPLATE;
023import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_COMMENT;
024import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_DIRECTORY;
025import static org.nuxeo.ecm.permissions.Constants.ACE_KEY;
026import static org.nuxeo.ecm.permissions.Constants.ACL_NAME_KEY;
027import static org.nuxeo.ecm.permissions.Constants.PERMISSION_NOTIFICATION_EVENT;
028
029import java.util.Collections;
030import java.util.Locale;
031
032import org.apache.commons.lang.StringEscapeUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.common.utils.i18n.I18NUtils;
036import org.nuxeo.ecm.automation.AutomationService;
037import org.nuxeo.ecm.automation.OperationChain;
038import org.nuxeo.ecm.automation.OperationContext;
039import org.nuxeo.ecm.automation.OperationException;
040import org.nuxeo.ecm.automation.core.operations.notification.SendMail;
041import org.nuxeo.ecm.automation.core.scripting.Expression;
042import org.nuxeo.ecm.automation.core.scripting.Scripting;
043import org.nuxeo.ecm.automation.core.util.StringList;
044import org.nuxeo.ecm.automation.features.PlatformFunctions;
045import org.nuxeo.ecm.core.api.CoreSession;
046import org.nuxeo.ecm.core.api.DocumentModel;
047import org.nuxeo.ecm.core.api.NuxeoGroup;
048import org.nuxeo.ecm.core.api.NuxeoPrincipal;
049import org.nuxeo.ecm.core.api.security.ACE;
050import org.nuxeo.ecm.core.event.Event;
051import org.nuxeo.ecm.core.event.EventBundle;
052import org.nuxeo.ecm.core.event.EventContext;
053import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener;
054import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
055import org.nuxeo.ecm.directory.Session;
056import org.nuxeo.ecm.directory.api.DirectoryService;
057import org.nuxeo.ecm.platform.ec.notification.service.NotificationService;
058import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper;
059import org.nuxeo.ecm.platform.usermanager.UserManager;
060import org.nuxeo.ecm.tokenauth.service.TokenAuthenticationService;
061import org.nuxeo.runtime.api.Framework;
062
063/**
064 * Listener sending an email notification for a granted ACE.
065 * <p>
066 * This listener checks only if the ACE is granted. It assumes that other checks (such as the ACE becomes effective)
067 * have been done before.
068 *
069 * @since 7.4
070 */
071public class PermissionGrantedNotificationListener implements PostCommitFilteringEventListener {
072
073    public static final String LABEL_SUBJECT_NEW_PERMISSION = "label.subject.new.permission";
074
075    private static final Log log = LogFactory.getLog(PermissionGrantedNotificationListener.class);
076
077    public static final String SUBJECT_FORMAT = "%s %s";
078
079    @Override
080    public void handleEvent(EventBundle events) {
081        for (Event event : events) {
082            handleEvent(event);
083        }
084    }
085
086    protected void handleEvent(Event event) {
087        EventContext eventCtx = event.getContext();
088        if (!(eventCtx instanceof DocumentEventContext)) {
089            return;
090        }
091
092        DocumentEventContext docCtx = (DocumentEventContext) eventCtx;
093        CoreSession coreSession = docCtx.getCoreSession();
094        DocumentModel doc = docCtx.getSourceDocument();
095        if (doc == null || !coreSession.exists(doc.getRef())) {
096            return;
097        }
098
099        ACE ace = (ACE) docCtx.getProperty(ACE_KEY);
100        String aclName = (String) docCtx.getProperty(ACL_NAME_KEY);
101        if (ace == null || ace.isDenied() || aclName == null) {
102            return;
103        }
104
105        String username = ace.getUsername();
106        StringList to = getRecipients(username);
107        if (to == null) {
108            // no recipient
109            return;
110        }
111
112        Expression from = Scripting.newExpression("Env[\"mail.from\"]");
113        NotificationService notificationService = NotificationServiceHelper.getNotificationService();
114        String subject = String.format(SUBJECT_FORMAT, notificationService.getEMailSubjectPrefix(),
115                I18NUtils.getMessageString("messages", LABEL_SUBJECT_NEW_PERMISSION, new Object[] { doc.getTitle() },
116                        Locale.ENGLISH));
117
118        try (OperationContext ctx = new OperationContext(coreSession)) {
119            ctx.setInput(doc);
120            ctx.put("ace", ace);
121
122            Framework.doPrivileged(() -> {
123                DirectoryService directoryService = Framework.getService(DirectoryService.class);
124                try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
125                    String id = PermissionHelper.computeDirectoryId(doc, aclName, ace.getId());
126                    DocumentModel entry = session.getEntry(id);
127                    if (entry != null) {
128                        String comment = (String) entry.getPropertyValue(ACE_INFO_COMMENT);
129                        if (comment != null) {
130                            comment = StringEscapeUtils.escapeHtml(comment);
131                            comment = comment.replaceAll("\n", "<br/>");
132                            ctx.put("comment", comment);
133                        }
134                    }
135                }
136            });
137
138            String aceCreator = ace.getCreator();
139            if (aceCreator != null) {
140                UserManager userManager = Framework.getService(UserManager.class);
141                NuxeoPrincipal creator = userManager.getPrincipal(aceCreator);
142                if (creator != null) {
143                    ctx.put("aceCreator", String.format("%s (%s)", principalFullName(creator), creator.getName()));
144                }
145            }
146
147            if (NuxeoPrincipal.isTransientUsername(username)) {
148                TokenAuthenticationService tokenAuthenticationService = Framework
149                        .getService(TokenAuthenticationService.class);
150                String token = tokenAuthenticationService.getToken(username, doc.getRepositoryName(), doc.getId());
151                if (token != null) {
152                    ctx.put("token", token);
153                }
154            }
155
156            OperationChain chain = new OperationChain("SendMail");
157
158            // if there is more than one recipient use "bcc:" instead of "to:" for avoiding email sharing
159            final boolean useBcc = to.size() > 1;
160            chain.add(SendMail.ID)
161                 .set("from", from)
162                 .set("to", useBcc ? null : to)
163                 .set("bcc", useBcc ? to : null)
164                 .set("HTML", true)
165                 .set("subject", subject)
166                 .set("message", ACE_GRANTED_TEMPLATE);
167            Framework.getService(AutomationService.class).run(ctx, chain);
168        } catch (OperationException e) {
169            log.warn("Unable to notify user", e);
170            log.debug(e, e);
171        }
172    }
173
174    // copied from org.nuxeo.ecm.platform.ui.web.tag.fn.Functions which lives in nuxeo-platform-ui-web
175    public static String principalFullName(NuxeoPrincipal principal) {
176        String first = principal.getFirstName();
177        String last = principal.getLastName();
178        return userDisplayName(principal.getName(), first, last);
179    }
180
181    public static String userDisplayName(String id, String first, String last) {
182        if (first == null || first.length() == 0) {
183            if (last == null || last.length() == 0) {
184                return id;
185            } else {
186                return last;
187            }
188        } else {
189            if (last == null || last.length() == 0) {
190                return first;
191            } else {
192                return first + ' ' + last;
193            }
194        }
195    }
196
197    protected StringList getRecipients(String username) {
198        UserManager userManager = Framework.getService(UserManager.class);
199        NuxeoPrincipal principal = userManager.getPrincipal(username);
200        StringList to = null;
201        if (principal != null) {
202            to = new StringList(Collections.singletonList(principal.getEmail()));
203        } else {
204            NuxeoGroup group = userManager.getGroup(username);
205            if (group != null) {
206                PlatformFunctions platformFunctions = new PlatformFunctions();
207                to = platformFunctions.getEmailsFromGroup(group.getName());
208            }
209        }
210        return to;
211    }
212
213    @Override
214    public boolean acceptEvent(Event event) {
215        String eventName = event.getName();
216        return PERMISSION_NOTIFICATION_EVENT.equals(eventName);
217    }
218}