001/*
002 * (C) Copyright 2012 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 *     Olivier Grisel <ogrisel@nuxeo.com>
018 */
019package org.nuxeo.drive.listener;
020
021import static org.nuxeo.ecm.core.api.trash.TrashService.DOCUMENT_TRASHED;
022
023import java.util.ArrayList;
024import java.util.Calendar;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import org.apache.logging.log4j.LogManager;
031import org.apache.logging.log4j.Logger;
032import org.nuxeo.drive.adapter.FileSystemItem;
033import org.nuxeo.drive.adapter.NuxeoDriveContribException;
034import org.nuxeo.drive.adapter.RootlessItemException;
035import org.nuxeo.drive.service.FileSystemChangeFinder;
036import org.nuxeo.drive.service.FileSystemItemAdapterService;
037import org.nuxeo.drive.service.NuxeoDriveEvents;
038import org.nuxeo.drive.service.impl.NuxeoDriveManagerImpl;
039import org.nuxeo.ecm.core.api.CoreSession;
040import org.nuxeo.ecm.core.api.DocumentModel;
041import org.nuxeo.ecm.core.api.LifeCycleConstants;
042import org.nuxeo.ecm.core.api.NuxeoPrincipal;
043import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
044import org.nuxeo.ecm.core.api.event.CoreEventConstants;
045import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
046import org.nuxeo.ecm.core.event.Event;
047import org.nuxeo.ecm.core.event.EventContext;
048import org.nuxeo.ecm.core.event.EventListener;
049import org.nuxeo.ecm.core.event.EventProducer;
050import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
051import org.nuxeo.ecm.core.event.impl.EventContextImpl;
052import org.nuxeo.ecm.core.query.sql.NXQL;
053import org.nuxeo.ecm.core.schema.FacetNames;
054import org.nuxeo.ecm.platform.audit.api.AuditLogger;
055import org.nuxeo.ecm.platform.audit.api.ExtendedInfo;
056import org.nuxeo.ecm.platform.audit.api.LogEntry;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * Synchronous event listener to track events that cannot be directly handled by the {@link FileSystemChangeFinder}
061 * because the document bound to the event is either no more adaptable as a {@link FileSystemItem} after the transaction
062 * has been committed (e.g. deletion) or not a descendant of a synchronization root (e.g. security update on any
063 * document). In particular this includes:
064 * <ul>
065 * <li>Synchronization root unregistration (user specific).</li>
066 * <li>Simple document or synchronization root trashed state change to trashed.</li>
067 * <li>Simple document or synchronization root physical removal from the directory.</li>
068 * <li>Update of a document after which it has no blob.</li>
069 * <li>Move of a document to a non synchronized folder.</li>
070 * <li>Security update.</li>
071 * <li>Group change.</li>
072 * </ul>
073 * <p>
074 * The listener injects virtual entries in the audit logs with the {@link NuxeoDriveEvents#EVENT_CATEGORY} category to
075 * be handled by the {@link FileSystemChangeFinder}. These entries are set in the context of a
076 * {@link NuxeoDriveEvents#VIRTUAL_EVENT_CREATED} event handled by the post-commit asynchronous
077 * {@link NuxeoDriveVirtualEventLogger} to ensure that the transaction is committed before the log entries are actually
078 * added.
079 */
080public class NuxeoDriveFileSystemDeletionListener implements EventListener {
081
082    private static final Logger log = LogManager.getLogger(NuxeoDriveFileSystemDeletionListener.class);
083
084    @Override
085    public void handleEvent(Event event) {
086        DocumentEventContext ctx;
087        if (event.getContext() instanceof DocumentEventContext) {
088            ctx = (DocumentEventContext) event.getContext();
089        } else {
090            // Not interested in events that are not related to documents
091            return;
092        }
093        DocumentModel doc = ctx.getSourceDocument();
094        if (doc.hasFacet(FacetNames.SYSTEM_DOCUMENT)) {
095            // Not interested in system documents
096            return;
097        }
098        DocumentModel docForLogEntry = doc;
099        if (DocumentEventTypes.BEFORE_DOC_UPDATE.equals(event.getName())) {
100            docForLogEntry = handleBeforeDocUpdate(ctx, doc);
101            if (docForLogEntry == null) {
102                return;
103            }
104        }
105        // Fallback on the transition event check
106        if (!DOCUMENT_TRASHED.equals(event.getName()) && LifeCycleConstants.TRANSITION_EVENT.equals(event.getName())
107                && !handleLifeCycleTransition(ctx)) {
108            return;
109        }
110        if (DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName()) && !handleAboutToRemove(doc)) {
111            return;
112        }
113        log.debug("NuxeoDriveFileSystemDeletionListener handling {} event for {}", event::getName, () -> doc);
114        // Virtual event name
115        String virtualEventName;
116        if (DocumentEventTypes.BEFORE_DOC_SECU_UPDATE.equals(event.getName())
117                || NuxeoDriveEvents.GROUP_UPDATED.equals(event.getName())) {
118            virtualEventName = NuxeoDriveEvents.SECURITY_UPDATED_EVENT;
119        } else if (DocumentEventTypes.ABOUT_TO_MOVE.equals(event.getName())) {
120            virtualEventName = NuxeoDriveEvents.MOVED_EVENT;
121        } else {
122            virtualEventName = NuxeoDriveEvents.DELETED_EVENT;
123        }
124        // Some events will only impact a specific user (e.g. root
125        // unregistration)
126        String impactedUserName = (String) ctx.getProperty(NuxeoDriveEvents.IMPACTED_USERNAME_PROPERTY);
127        fireVirtualEventLogEntries(docForLogEntry, virtualEventName, ctx.getPrincipal(), impactedUserName,
128                ctx.getCoreSession());
129    }
130
131    protected DocumentModel handleBeforeDocUpdate(DocumentEventContext ctx, DocumentModel doc) {
132        // Interested in update of a BlobHolder whose blob has been removed
133        boolean blobRemoved = false;
134        DocumentModel previousDoc = (DocumentModel) ctx.getProperty(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL);
135        if (previousDoc != null) {
136            BlobHolder previousBh = previousDoc.getAdapter(BlobHolder.class);
137            if (previousBh != null) {
138                BlobHolder bh = doc.getAdapter(BlobHolder.class);
139                if (bh != null) {
140                    blobRemoved = previousBh.getBlob() != null && bh.getBlob() == null;
141                }
142            }
143        }
144        if (blobRemoved) {
145            // Use previous doc holding a Blob for it to be adaptable as a
146            // FileSystemItem
147            return previousDoc;
148        } else {
149            return null;
150        }
151    }
152
153    protected boolean handleLifeCycleTransition(DocumentEventContext ctx) {
154        String transition = (String) ctx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION);
155        // Interested in 'deleted' life cycle transition only
156        return transition != null && LifeCycleConstants.DELETE_TRANSITION.equals(transition);
157
158    }
159
160    protected boolean handleAboutToRemove(DocumentModel doc) {
161        // Document deletion of document that are already in the trash should not be marked as FS deletion to avoid
162        // duplicates
163        return !doc.isTrashed();
164    }
165
166    protected void fireVirtualEventLogEntries(DocumentModel doc, String eventName, NuxeoPrincipal principal,
167            String impactedUserName, CoreSession session) {
168
169        if (Framework.getService(AuditLogger.class) == null) {
170            // The log is not deployed (probably in unittest)
171            return;
172        }
173
174        List<LogEntry> entries = new ArrayList<>();
175        // XXX: shall we use the server local for the event date or UTC?
176        Date currentDate = Calendar.getInstance(NuxeoDriveManagerImpl.UTC).getTime();
177        FileSystemItem fsItem = getFileSystemItem(doc, eventName);
178        if (fsItem == null) {
179            // NXP-21373: Let's check if we need to propagate the securityUpdated virtual event to child synchronization
180            // roots in order to make Drive add / remove them if needed
181            if (NuxeoDriveEvents.SECURITY_UPDATED_EVENT.equals(eventName)) {
182                for (DocumentModel childSyncRoot : getChildSyncRoots(doc, session)) {
183                    FileSystemItem childSyncRootFSItem = getFileSystemItem(childSyncRoot, eventName);
184                    if (childSyncRootFSItem != null) {
185                        entries.add(computeLogEntry(eventName, currentDate, childSyncRoot.getId(),
186                                childSyncRoot.getPathAsString(), principal.getName(), childSyncRoot.getType(),
187                                childSyncRoot.getRepositoryName(), childSyncRoot.getCurrentLifeCycleState(),
188                                impactedUserName, childSyncRootFSItem));
189                    }
190                }
191            }
192        } else {
193            entries.add(computeLogEntry(eventName, currentDate, doc.getId(), doc.getPathAsString(), principal.getName(),
194                    doc.getType(), doc.getRepositoryName(), doc.getCurrentLifeCycleState(), impactedUserName, fsItem));
195        }
196
197        if (!entries.isEmpty()) {
198            EventContext eventContext = new EventContextImpl(entries.toArray());
199            Event event = eventContext.newEvent(NuxeoDriveEvents.VIRTUAL_EVENT_CREATED);
200            Framework.getService(EventProducer.class).fireEvent(event);
201        }
202    }
203
204    protected FileSystemItem getFileSystemItem(DocumentModel doc, String eventName) {
205        try {
206            // NXP-19442: Avoid useless and costly call to DocumentModel#getLockInfo
207            return Framework.getService(FileSystemItemAdapterService.class).getFileSystemItem(doc, true, true, false);
208        } catch (RootlessItemException e) {
209            // can happen when deleting a folder under and unregistered root:
210            // nothing to do
211            return null;
212        } catch (NuxeoDriveContribException e) {
213            // Nuxeo Drive contributions missing or component not ready
214            log.debug(
215                    "Either Nuxeo Drive contributions are missing or the FileSystemItemAdapterService component is not ready (application has nor started yet) => ignoring event '{}'.",
216                    eventName);
217            return null;
218        }
219    }
220
221    protected List<DocumentModel> getChildSyncRoots(DocumentModel doc, CoreSession session) {
222        if (doc.getParentRef() == null) {
223            return new ArrayList<>();
224        }
225        String nxql = "SELECT * FROM Document WHERE ecm:mixinType = '" + NuxeoDriveManagerImpl.NUXEO_DRIVE_FACET
226                + "' AND ecm:isTrashed = 0 AND ecm:isVersion = 0 AND ecm:path STARTSWITH "
227                + NXQL.escapeString(doc.getPathAsString());
228        return session.query(nxql);
229    }
230
231    protected LogEntry computeLogEntry(String eventName, Date eventDate, String docId, String docPath, String principal,
232            String docType, String repositoryName, String currentLifeCycleState, String impactedUserName,
233            FileSystemItem fsItem) {
234
235        AuditLogger logger = Framework.getService(AuditLogger.class);
236        LogEntry entry = logger.newLogEntry();
237        entry.setEventId(eventName);
238        entry.setEventDate(eventDate);
239        entry.setCategory(NuxeoDriveEvents.EVENT_CATEGORY);
240        entry.setDocUUID(docId);
241        entry.setDocPath(docPath);
242        entry.setPrincipalName(principal);
243        entry.setDocType(docType);
244        entry.setRepositoryId(repositoryName);
245        entry.setDocLifeCycle(currentLifeCycleState);
246
247        Map<String, ExtendedInfo> extendedInfos = new HashMap<>();
248        if (impactedUserName != null) {
249            extendedInfos.put("impactedUserName", logger.newExtendedInfo(impactedUserName));
250        }
251        // We do not serialize the whole object as it's too big to fit in a
252        // StringInfo column
253        extendedInfos.put("fileSystemItemId", logger.newExtendedInfo(fsItem.getId()));
254        extendedInfos.put("fileSystemItemName", logger.newExtendedInfo(fsItem.getName()));
255        entry.setExtendedInfos(extendedInfos);
256
257        return entry;
258    }
259
260}