001/*
002 * (C) Copyright 2006-2017 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.apache.commons.lang3.StringUtils;
030import org.nuxeo.ecm.core.api.Blob;
031import org.nuxeo.ecm.core.api.CoreSession;
032import org.nuxeo.ecm.core.api.DocumentModel;
033import org.nuxeo.ecm.core.api.DocumentSecurityException;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.core.api.PathRef;
036import org.nuxeo.ecm.core.api.VersioningOption;
037import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
038import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
039import org.nuxeo.ecm.core.blob.BlobManager;
040import org.nuxeo.ecm.core.blob.BlobProvider;
041import org.nuxeo.ecm.platform.filemanager.api.FileImporterContext;
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
083    protected transient FileManagerService fileManagerService;
084
085    @Override
086    public List<String> getFilters() {
087        return filters;
088    }
089
090    @Override
091    public void setFilters(List<String> filters) {
092        this.filters = filters;
093        patterns = new ArrayList<>();
094        for (String filter : filters) {
095            patterns.add(Pattern.compile(filter));
096        }
097    }
098
099    @Override
100    public boolean matches(String mimeType) {
101        for (Pattern pattern : patterns) {
102            if (pattern.matcher(mimeType).matches()) {
103                return true;
104            }
105        }
106        return false;
107    }
108
109    @Override
110    public String getName() {
111        return name;
112    }
113
114    @Override
115    public void setName(String name) {
116        this.name = name;
117    }
118
119    @Override
120    public String getDocType() {
121        return docType;
122    }
123
124    @Override
125    public void setDocType(String docType) {
126        this.docType = docType;
127    }
128
129    /**
130     * Gets the doc type to use in the given container.
131     */
132    protected String getDocType(DocumentModel container) { // NOSONAR
133        return getDocType(); // use XML configuration
134    }
135
136    /**
137     * Default document type to use when the plugin XML configuration does not specify one.
138     * <p>
139     * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used.
140     */
141    protected String getDefaultDocType() {
142        throw new UnsupportedOperationException();
143    }
144
145    /**
146     * Whether document overwrite is detected by checking title or filename.
147     * <p>
148     * To implement when the default {@link #createOrUpdate(FileImporterContext)} method is used.
149     */
150    protected boolean isOverwriteByTitle() {
151        throw new UnsupportedOperationException();
152    }
153
154    /**
155     * Creates the document (sets its properties). {@link #updateDocument} will be called after this.
156     * <p>
157     * Default implementation sets the title.
158     */
159    protected void createDocument(DocumentModel doc, String title) {
160        doc.setPropertyValue("dc:title", title);
161    }
162
163    /**
164     * Tries to update the document <code>doc</code> with the blob <code>content</code>.
165     * <p>
166     * Returns <code>true</code> if the document is really updated.
167     *
168     * @since 7.1
169     */
170    protected boolean updateDocumentIfPossible(DocumentModel doc, Blob content) {
171        updateDocument(doc, content);
172        return true;
173    }
174
175    /**
176     * Updates the document (sets its properties).
177     * <p>
178     * Default implementation sets the content.
179     */
180    protected void updateDocument(DocumentModel doc, Blob content) {
181        doc.getAdapter(BlobHolder.class).setBlob(content);
182    }
183
184    protected Blob getBlob(DocumentModel doc) {
185        return doc.getAdapter(BlobHolder.class).getBlob();
186    }
187
188    @Override
189    public boolean isOneToMany() {
190        return false;
191    }
192
193    @Override
194    public DocumentModel create(CoreSession session, Blob content, String path, boolean overwrite, String fullname,
195            TypeManager typeService) throws IOException {
196        FileImporterContext context = FileImporterContext.builder(session, content, path)
197                                                         .overwrite(overwrite)
198                                                         .fileName(fullname)
199                                                         .build();
200        return createOrUpdate(context);
201    }
202
203    @Override
204    public DocumentModel createOrUpdate(FileImporterContext context) throws IOException {
205        CoreSession session = context.getSession();
206        String path = getNearestContainerPath(session, context.getParentPath());
207        DocumentModel container = session.getDocument(new PathRef(path));
208        String targetDocType = getDocType(container); // from override or descriptor
209        if (targetDocType == null) {
210            targetDocType = getDefaultDocType();
211        }
212        doSecurityCheck(session, path, targetDocType);
213
214        Blob blob = context.getBlob();
215        String filename = FileManagerUtils.fetchFileName(
216                StringUtils.defaultIfBlank(context.getFileName(), blob.getFilename()));
217        String title = FileManagerUtils.fetchTitle(filename);
218        blob.setFilename(filename);
219        // look for an existing document with same title or filename
220        DocumentModel doc;
221        if (isOverwriteByTitle()) {
222            doc = FileManagerUtils.getExistingDocByTitle(session, path, title);
223        } else {
224            doc = FileManagerUtils.getExistingDocByFileName(session, path, filename);
225        }
226        if (context.isOverwrite() && doc != null) {
227            Blob previousBlob = getBlob(doc);
228            // check that previous blob allows overwrite
229            if (previousBlob != null) {
230                BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(previousBlob);
231                if (blobProvider != null && !blobProvider.supportsUserUpdate()) {
232                    throw new DocumentSecurityException("Cannot overwrite blob");
233                }
234            }
235            // update data
236            boolean isDocumentUpdated = updateDocumentIfPossible(doc, blob);
237            if (!isDocumentUpdated) {
238                return null;
239            }
240            if (Framework.isBooleanPropertyTrue(SKIP_UPDATE_AUDIT_LOGGING)) {
241                // skip the update event if configured to do so
242                doc.putContextData(DISABLE_AUDIT_LOGGER, true);
243            }
244            if (context.isPersistDocument()) {
245                // save
246                doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName());
247                doc = doc.getCoreSession().saveDocument(doc);
248                session.save();
249            }
250        } else {
251            // create document model
252            doc = session.createDocumentModel(targetDocType);
253            createDocument(doc, title);
254            // set path
255            PathSegmentService pss = Framework.getService(PathSegmentService.class);
256            doc.setPathInfo(path, pss.generatePathSegment(doc));
257            // update data
258            updateDocument(doc, blob);
259            if (context.isPersistDocument()) {
260                // create
261                doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName());
262                doc = session.createDocument(doc);
263                session.save();
264            }
265        }
266        return doc;
267    }
268
269    /**
270     * Avoid checkin for a 0-length blob. Microsoft-WebDAV-MiniRedir first creates a 0-length file and then locks it
271     * before putting the real file. But we don't want this first placeholder to cause a versioning event.
272     *
273     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
274     *             behaviors from importers
275     */
276    @Deprecated
277    protected boolean skipCheckInForBlob(Blob blob) {
278        return blob == null || blob.getLength() == 0;
279    }
280
281    /**
282     * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed
283     */
284    @Deprecated
285    public FileManagerService getFileManagerService() {
286        return fileManagerService;
287    }
288
289    /**
290     * @deprecated since 10.3, use {@link Framework#getService(Class)} instead if needed
291     */
292    @Deprecated
293    @Override
294    public void setFileManagerService(FileManagerService fileManagerService) {
295        this.fileManagerService = fileManagerService;
296    }
297
298    @Override
299    public void setEnabled(boolean enabled) {
300        this.enabled = enabled;
301    }
302
303    @Override
304    public boolean isEnabled() {
305        return enabled;
306    }
307
308    @Override
309    public Integer getOrder() {
310        return order;
311    }
312
313    @Override
314    public void setOrder(Integer order) {
315        this.order = order;
316    }
317
318    @Override
319    public int compareTo(FileImporter other) {
320        Integer otherOrder = other.getOrder();
321        if (order == null && otherOrder == null) {
322            return 0;
323        } else if (order == null) {
324            return 1;
325        } else if (otherOrder == null) {
326            return -1;
327        }
328        return order.compareTo(otherOrder);
329    }
330
331    /**
332     * Returns nearest container path
333     * <p>
334     * If given path points to a folderish document, return it. Else, return parent path.
335     */
336    protected String getNearestContainerPath(CoreSession documentManager, String path) {
337        DocumentModel currentDocument = documentManager.getDocument(new PathRef(path));
338        if (!currentDocument.isFolder()) {
339            path = path.substring(0, path.lastIndexOf('/'));
340        }
341        return path;
342    }
343
344    /**
345     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
346     *             behaviors from importers
347     */
348    @Deprecated
349    protected void checkIn(DocumentModel doc) {
350        VersioningOption option = fileManagerService.getVersioningOption();
351        if (option != null && option != VersioningOption.NONE && doc.isCheckedOut()) {
352            doc.checkIn(option, null);
353        }
354    }
355
356    /**
357     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
358     *             behaviors from importers
359     */
360    @Deprecated
361    protected void checkInAfterAdd(DocumentModel doc) {
362        if (fileManagerService.doVersioningAfterAdd()) {
363            checkIn(doc);
364        }
365    }
366
367    /**
368     * @since 10.10
369     */
370    protected void doSecurityCheck(CoreSession documentManager, String path, String typeName) {
371        doSecurityCheck(documentManager, path, typeName, Framework.getService(TypeManager.class));
372    }
373
374    protected void doSecurityCheck(CoreSession documentManager, String path, String typeName, TypeManager typeService) {
375        // perform the security checks
376        PathRef containerRef = new PathRef(path);
377        if (!documentManager.hasPermission(containerRef, READ_PROPERTIES)
378                || !documentManager.hasPermission(containerRef, ADD_CHILDREN)) {
379            throw new DocumentSecurityException("Not enough rights to create folder");
380        }
381        DocumentModel container = documentManager.getDocument(containerRef);
382
383        Type containerType = typeService.getType(container.getType());
384        if (containerType == null) {
385            return;
386        }
387
388        if (!typeService.isAllowedSubType(typeName, container.getType(), container)) {
389            throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s",
390                    typeName, containerType.getId()));
391        }
392    }
393
394}