001/*
002 * (C) Copyright 2019 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 */
019package org.nuxeo.ecm.platform.filemanager.service.extension;
020
021import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN;
022import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.regex.Pattern;
028
029import org.nuxeo.ecm.core.api.Blob;
030import org.nuxeo.ecm.core.api.CoreSession;
031import org.nuxeo.ecm.core.api.DocumentModel;
032import org.nuxeo.ecm.core.api.DocumentSecurityException;
033import org.nuxeo.ecm.core.api.NuxeoException;
034import org.nuxeo.ecm.core.api.PathRef;
035import org.nuxeo.ecm.core.api.VersioningOption;
036import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
037import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
038import org.nuxeo.ecm.core.blob.BlobManager;
039import org.nuxeo.ecm.core.blob.BlobProvider;
040import org.nuxeo.ecm.platform.filemanager.api.FileImporterContext;
041import org.nuxeo.ecm.platform.filemanager.api.FileManager;
042import org.nuxeo.ecm.platform.filemanager.service.FileManagerService;
043import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils;
044import org.nuxeo.ecm.platform.types.Type;
045import org.nuxeo.ecm.platform.types.TypeManager;
046import org.nuxeo.runtime.api.Framework;
047
048/**
049 * File importer abstract class.
050 * <p>
051 * Default file importer behavior.
052 *
053 * @see FileImporter
054 * @author <a href="mailto:akalogeropoulos@nuxeo.com">Andreas Kalogeropolos</a>
055 */
056public abstract class AbstractFileImporter implements FileImporter {
057
058    private static final long serialVersionUID = 1L;
059
060    protected String name = "";
061
062    protected String docType;
063
064    protected transient List<String> filters = new ArrayList<>();
065
066    protected transient List<Pattern> patterns;
067
068    protected boolean enabled = true;
069
070    protected Integer order = 0;
071
072    public static final String SKIP_UPDATE_AUDIT_LOGGING = "org.nuxeo.filemanager.skip.audit.logging.forupdates";
073
074    // duplicated from Audit module to avoid circular dependency
075    public static final String DISABLE_AUDIT_LOGGER = "disableAuditLogger";
076
077    // to be used by plugin implementation to gain access to standard file
078    // creation utility methods without having to lookup the service
079    /**
080     * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed
081     */
082    @Deprecated(since = "10.3")
083    protected transient FileManagerService fileManagerService;
084
085    protected AbstractFileImporter() {
086        this.fileManagerService = (FileManagerService) Framework.getService(FileManager.class);
087    }
088
089    @Override
090    public List<String> getFilters() {
091        return filters;
092    }
093
094    @Override
095    public void setFilters(List<String> filters) {
096        this.filters = filters;
097        patterns = new ArrayList<>();
098        for (String filter : filters) {
099            patterns.add(Pattern.compile(filter));
100        }
101    }
102
103    @Override
104    public boolean matches(String mimeType) {
105        for (Pattern pattern : patterns) {
106            if (pattern.matcher(mimeType).matches()) {
107                return true;
108            }
109        }
110        return false;
111    }
112
113    @Override
114    public String getName() {
115        return name;
116    }
117
118    @Override
119    public void setName(String name) {
120        this.name = name;
121    }
122
123    @Override
124    public String getDocType() {
125        return docType;
126    }
127
128    @Override
129    public void setDocType(String docType) {
130        this.docType = docType;
131    }
132
133    /**
134     * Gets the doc type to use in the given container.
135     */
136    protected String getDocType(DocumentModel container) { // NOSONAR
137        return getDocType(); // use XML configuration
138    }
139
140    /**
141     * Default document type to use when the plugin XML configuration does not specify one.
142     * <p>
143     * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used.
144     */
145    protected String getDefaultDocType() {
146        throw new UnsupportedOperationException();
147    }
148
149    /**
150     * Whether document overwrite is detected by checking title or filename.
151     * <p>
152     * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used.
153     */
154    protected boolean isOverwriteByTitle() {
155        throw new UnsupportedOperationException();
156    }
157
158    /**
159     * Creates the document (sets its properties). {@link #updateDocument} will be called after this.
160     * <p>
161     * Default implementation sets the title.
162     */
163    protected void createDocument(DocumentModel doc, String title) {
164        doc.setPropertyValue("dc:title", title);
165    }
166
167    /**
168     * Tries to update the document <code>doc</code> with the blob <code>content</code>.
169     * <p>
170     * Returns <code>true</code> if the document is really updated.
171     *
172     * @since 7.1
173     */
174    protected boolean updateDocumentIfPossible(DocumentModel doc, Blob content) {
175        updateDocument(doc, content);
176        return true;
177    }
178
179    /**
180     * Updates the document (sets its properties).
181     * <p>
182     * Default implementation sets the content.
183     */
184    protected void updateDocument(DocumentModel doc, Blob content) {
185        doc.getAdapter(BlobHolder.class).setBlob(content);
186    }
187
188    protected Blob getBlob(DocumentModel doc) {
189        return doc.getAdapter(BlobHolder.class).getBlob();
190    }
191
192    @Override
193    public boolean isOneToMany() {
194        return false;
195    }
196
197    @Override
198    public DocumentModel create(CoreSession session, Blob content, String path, boolean overwrite, String fullname,
199            TypeManager typeService) throws IOException {
200        FileImporterContext context = FileImporterContext.builder(session, content, path)
201                                                         .overwrite(overwrite)
202                                                         .fileName(fullname)
203                                                         .build();
204        return createOrUpdate(context);
205    }
206
207    @Override
208    public DocumentModel createOrUpdate(FileImporterContext context) throws IOException {
209        CoreSession session = context.getSession();
210        String path = getNearestContainerPath(session, context.getParentPath());
211        DocumentModel container = session.getDocument(new PathRef(path));
212        String targetDocType = getDocType(container); // from override or descriptor
213        if (targetDocType == null) {
214            targetDocType = getDefaultDocType();
215        }
216        // always check security
217        checkSecurity(session, path);
218        // check allowed subtypes unless bypassed
219        if (!context.isBypassAllowedSubtypeCheck()) {
220            checkAllowedSubtypes(session, path, targetDocType);
221        }
222
223        Blob blob = context.getBlob();
224        String filename = FileManagerUtils.fetchFileName(context.getFileName());
225        String title = FileManagerUtils.fetchTitle(filename);
226        blob.setFilename(filename);
227        // look for an existing document with same title or filename
228        DocumentModel doc;
229        if (isOverwriteByTitle()) {
230            doc = FileManagerUtils.getExistingDocByTitle(session, path, title);
231        } else {
232            doc = FileManagerUtils.getExistingDocByFileName(session, path, filename);
233        }
234        if (context.isOverwrite() && doc != null) {
235            Blob previousBlob = getBlob(doc);
236            // check that previous blob allows overwrite
237            if (previousBlob != null) {
238                BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(previousBlob);
239                if (blobProvider != null && !blobProvider.supportsUserUpdate()) {
240                    throw new DocumentSecurityException("Cannot overwrite blob");
241                }
242            }
243            // update data
244            boolean isDocumentUpdated = updateDocumentIfPossible(doc, blob);
245            if (!isDocumentUpdated) {
246                return null;
247            }
248            if (Framework.isBooleanPropertyTrue(SKIP_UPDATE_AUDIT_LOGGING)) {
249                // skip the update event if configured to do so
250                doc.putContextData(DISABLE_AUDIT_LOGGER, true);
251            }
252            if (context.isPersistDocument()) {
253                // save
254                doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName());
255                doc = doc.getCoreSession().saveDocument(doc);
256                session.save();
257            }
258        } else {
259            // create document model
260            doc = session.createDocumentModel(targetDocType);
261            createDocument(doc, title);
262            // set path
263            PathSegmentService pss = Framework.getService(PathSegmentService.class);
264            doc.setPathInfo(path, pss.generatePathSegment(doc));
265            // update data
266            updateDocument(doc, blob);
267            if (context.isPersistDocument()) {
268                // create
269                doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName());
270                doc = session.createDocument(doc);
271                session.save();
272            }
273        }
274        return doc;
275    }
276
277    /**
278     * Avoid checkin for a 0-length blob. Microsoft-WebDAV-MiniRedir first creates a 0-length file and then locks it
279     * before putting the real file. But we don't want this first placeholder to cause a versioning event.
280     *
281     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
282     *             behaviors from importers
283     */
284    @Deprecated(since = "9.1")
285    protected boolean skipCheckInForBlob(Blob blob) {
286        return blob == null || blob.getLength() == 0;
287    }
288
289    /**
290     * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed
291     */
292    @Deprecated(since = "10.3")
293    public FileManagerService getFileManagerService() {
294        return fileManagerService;
295    }
296
297    /**
298     * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed
299     */
300    @Deprecated(since = "10.3")
301    @Override
302    public void setFileManagerService(FileManagerService fileManagerService) {
303        this.fileManagerService = fileManagerService;
304    }
305
306    @Override
307    public void setEnabled(boolean enabled) {
308        this.enabled = enabled;
309    }
310
311    @Override
312    public boolean isEnabled() {
313        return enabled;
314    }
315
316    @Override
317    public Integer getOrder() {
318        return order;
319    }
320
321    @Override
322    public void setOrder(Integer order) {
323        this.order = order;
324    }
325
326    @Override
327    public int compareTo(FileImporter other) {
328        Integer otherOrder = other.getOrder();
329        if (order == null && otherOrder == null) {
330            return 0;
331        } else if (order == null) {
332            return 1;
333        } else if (otherOrder == null) {
334            return -1;
335        }
336        return order.compareTo(otherOrder);
337    }
338
339    /**
340     * Returns nearest container path
341     * <p>
342     * If given path points to a folderish document, return it. Else, return parent path.
343     */
344    protected String getNearestContainerPath(CoreSession documentManager, String path) {
345        DocumentModel currentDocument = documentManager.getDocument(new PathRef(path));
346        if (!currentDocument.isFolder()) {
347            path = path.substring(0, path.lastIndexOf('/'));
348        }
349        return path;
350    }
351
352    /**
353     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
354     *             behaviors from importers
355     */
356    @Deprecated(since = "9.1")
357    protected void checkIn(DocumentModel doc) {
358        VersioningOption option = fileManagerService.getVersioningOption();
359        if (option != null && option != VersioningOption.NONE && doc.isCheckedOut()) {
360            doc.checkIn(option, null);
361        }
362    }
363
364    /**
365     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
366     *             behaviors from importers
367     */
368    @Deprecated(since = "9.1")
369    protected void checkInAfterAdd(DocumentModel doc) {
370        if (fileManagerService.doVersioningAfterAdd()) {
371            checkIn(doc);
372        }
373    }
374
375    /**
376     * @since 10.10
377     * @deprecated since 11.3, use {@link #checkSecurity(CoreSession, String) and #checkAllowedSubtypes(CoreSession,
378     *             String, String)} instead
379     */
380    @Deprecated
381    protected void doSecurityCheck(CoreSession documentManager, String path, String typeName) {
382        doSecurityCheck(documentManager, path, typeName, Framework.getService(TypeManager.class));
383    }
384
385    /**
386     * @deprecated since 11.3, use {@link #checkSecurity(CoreSession, String) and #checkAllowedSubtypes(CoreSession,
387     *             String, String)} instead
388     */
389    @Deprecated
390    protected void doSecurityCheck(CoreSession documentManager, String path, String typeName, TypeManager typeService) {
391        // perform the security checks
392        PathRef containerRef = new PathRef(path);
393        if (!documentManager.hasPermission(containerRef, READ_PROPERTIES)
394                || !documentManager.hasPermission(containerRef, ADD_CHILDREN)) {
395            throw new DocumentSecurityException("Not enough rights to create folder");
396        }
397        DocumentModel container = documentManager.getDocument(containerRef);
398
399        Type containerType = typeService.getType(container.getType());
400        if (containerType == null) {
401            return;
402        }
403
404        if (!typeService.isAllowedSubType(typeName, container.getType(), container)) {
405            throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s",
406                    typeName, containerType.getId()));
407        }
408    }
409
410    /**
411     * @since 11.3
412     */
413    protected void checkSecurity(CoreSession session, String path) {
414        PathRef containerRef = new PathRef(path);
415        if (!session.hasPermission(containerRef, ADD_CHILDREN)) {
416            throw new DocumentSecurityException("Not enough rights to create document");
417        }
418    }
419
420    /**
421     * @since 11.3
422     */
423    protected void checkAllowedSubtypes(CoreSession session, String path, String typeName) {
424        PathRef containerRef = new PathRef(path);
425        DocumentModel container = session.getDocument(containerRef);
426        TypeManager typeService = Framework.getService(TypeManager.class);
427        Type containerType = typeService.getType(container.getType());
428        if (containerType == null) {
429            return;
430        }
431
432        if (!typeService.isAllowedSubType(typeName, container.getType(), container)) {
433            throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s",
434                    typeName, containerType.getId()));
435        }
436    }
437
438}