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.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.service.FileManagerService;
041import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils;
042import org.nuxeo.ecm.platform.types.Type;
043import org.nuxeo.ecm.platform.types.TypeManager;
044import org.nuxeo.runtime.api.Framework;
045
046/**
047 * File importer abstract class.
048 * <p>
049 * Default file importer behavior.
050 *
051 * @see FileImporter
052 * @author <a href="mailto:akalogeropoulos@nuxeo.com">Andreas Kalogeropolos</a>
053 */
054public abstract class AbstractFileImporter implements FileImporter {
055
056    private static final long serialVersionUID = 1L;
057
058    protected String name = "";
059
060    protected String docType;
061
062    protected List<String> filters = new ArrayList<>();
063
064    protected List<Pattern> patterns;
065
066    protected boolean enabled = true;
067
068    protected Integer order = 0;
069
070    public static final String SKIP_UPDATE_AUDIT_LOGGING = "org.nuxeo.filemanager.skip.audit.logging.forupdates";
071
072    // duplicated from Audit module to avoid circular dependency
073    public static final String DISABLE_AUDIT_LOGGER = "disableAuditLogger";
074
075    // to be used by plugin implementation to gain access to standard file
076    // creation utility methods without having to lookup the service
077    protected FileManagerService fileManagerService;
078
079    public List<String> getFilters() {
080        return filters;
081    }
082
083    public void setFilters(List<String> filters) {
084        this.filters = filters;
085        patterns = new ArrayList<>();
086        for (String filter : filters) {
087            patterns.add(Pattern.compile(filter));
088        }
089    }
090
091    public boolean matches(String mimeType) {
092        for (Pattern pattern : patterns) {
093            if (pattern.matcher(mimeType).matches()) {
094                return true;
095            }
096        }
097        return false;
098    }
099
100    public String getName() {
101        return name;
102    }
103
104    public void setName(String name) {
105        this.name = name;
106    }
107
108    public String getDocType() {
109        return docType;
110    }
111
112    public void setDocType(String docType) {
113        this.docType = docType;
114    }
115
116    /**
117     * Gets the doc type to use in the given container.
118     */
119    public String getDocType(DocumentModel container) {
120        return getDocType(); // use XML configuration
121    }
122
123    /**
124     * Default document type to use when the plugin XML configuration does not specify one.
125     * <p>
126     * To implement when the default {@link #create} method is used.
127     */
128    public String getDefaultDocType() {
129        throw new UnsupportedOperationException();
130    }
131
132    /**
133     * Whether document overwrite is detected by checking title or filename.
134     * <p>
135     * To implement when the default {@link #create} method is used.
136     */
137    public boolean isOverwriteByTitle() {
138        throw new UnsupportedOperationException();
139    }
140
141    /**
142     * Creates the document (sets its properties). {@link #updateDocument} will be called after this.
143     * <p>
144     * Default implementation sets the title.
145     */
146    public void createDocument(DocumentModel doc, Blob content, String title) {
147        doc.setPropertyValue("dc:title", title);
148    }
149
150    /**
151     * Tries to update the document <code>doc</code> with the blob <code>content</code>.
152     * <p>
153     * Returns <code>true</code> if the document is really updated.
154     *
155     * @since 7.1
156     */
157    public boolean updateDocumentIfPossible(DocumentModel doc, Blob content) {
158        updateDocument(doc, content);
159        return true;
160    }
161
162    /**
163     * Updates the document (sets its properties).
164     * <p>
165     * Default implementation sets the content.
166     */
167    public void updateDocument(DocumentModel doc, Blob content) {
168        doc.getAdapter(BlobHolder.class).setBlob(content);
169    }
170
171    public Blob getBlob(DocumentModel doc) {
172        return doc.getAdapter(BlobHolder.class).getBlob();
173    }
174
175    @Override
176    public DocumentModel create(CoreSession session, Blob content, String path, boolean overwrite, String fullname,
177            TypeManager typeService) throws IOException {
178        path = getNearestContainerPath(session, path);
179        DocumentModel container = session.getDocument(new PathRef(path));
180        String docType = getDocType(container); // from override or descriptor
181        if (docType == null) {
182            docType = getDefaultDocType();
183        }
184        doSecurityCheck(session, path, docType, typeService);
185        String filename = FileManagerUtils.fetchFileName(fullname);
186        String title = FileManagerUtils.fetchTitle(filename);
187        content.setFilename(filename);
188        // look for an existing document with same title or filename
189        DocumentModel doc;
190        if (isOverwriteByTitle()) {
191            doc = FileManagerUtils.getExistingDocByTitle(session, path, title);
192        } else {
193            doc = FileManagerUtils.getExistingDocByFileName(session, path, filename);
194        }
195        if (overwrite && doc != null) {
196            Blob previousBlob = getBlob(doc);
197            // check that previous blob allows overwrite
198            if (previousBlob != null) {
199                BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(previousBlob);
200                if (blobProvider != null && !blobProvider.supportsUserUpdate()) {
201                    throw new DocumentSecurityException("Cannot overwrite blob");
202                }
203            }
204            // update data
205            boolean isDocumentUpdated = updateDocumentIfPossible(doc, content);
206            if (!isDocumentUpdated) {
207                return null;
208            }
209            if (Framework.isBooleanPropertyTrue(SKIP_UPDATE_AUDIT_LOGGING)) {
210                // skip the update event if configured to do so
211                doc.putContextData(DISABLE_AUDIT_LOGGER, true);
212            }
213            // save
214            doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName());
215            doc = doc.getCoreSession().saveDocument(doc);
216        } else {
217            // create document model
218            doc = session.createDocumentModel(docType);
219            createDocument(doc, content, title);
220            // set path
221            PathSegmentService pss = Framework.getService(PathSegmentService.class);
222            doc.setPathInfo(path, pss.generatePathSegment(doc));
223            // update data
224            updateDocument(doc, content);
225            // create
226            doc.putContextData(CoreSession.SOURCE, "fileimporter-" + getName());
227            doc = session.createDocument(doc);
228        }
229        session.save();
230        return doc;
231    }
232
233    /**
234     * Avoid checkin for a 0-length blob. Microsoft-WebDAV-MiniRedir first creates a 0-length file and then locks it
235     * before putting the real file. But we don't want this first placeholder to cause a versioning event.
236     *
237     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
238     * behaviors from importers
239     */
240    @Deprecated
241    protected boolean skipCheckInForBlob(Blob blob) {
242        return blob == null || blob.getLength() == 0;
243    }
244
245    public FileManagerService getFileManagerService() {
246        return fileManagerService;
247    }
248
249    public void setFileManagerService(FileManagerService fileManagerService) {
250        this.fileManagerService = fileManagerService;
251    }
252
253    public void setEnabled(boolean enabled) {
254        this.enabled = enabled;
255    }
256
257    public boolean isEnabled() {
258        return enabled;
259    }
260
261    public Integer getOrder() {
262        return order;
263    }
264
265    public void setOrder(Integer order) {
266        this.order = order;
267    }
268
269    public int compareTo(FileImporter other) {
270        Integer otherOrder = other.getOrder();
271        if (order == null && otherOrder == null) {
272            return 0;
273        } else if (order == null) {
274            return 1;
275        } else if (otherOrder == null) {
276            return -1;
277        }
278        return order.compareTo(otherOrder);
279    }
280
281    /**
282     * Returns nearest container path
283     * <p>
284     * If given path points to a folderish document, return it. Else, return parent path.
285     */
286    protected String getNearestContainerPath(CoreSession documentManager, String path) {
287        DocumentModel currentDocument = documentManager.getDocument(new PathRef(path));
288        if (!currentDocument.isFolder()) {
289            path = path.substring(0, path.lastIndexOf('/'));
290        }
291        return path;
292    }
293
294    /**
295     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
296     * behaviors from importers
297     */
298    @Deprecated
299    protected void checkIn(DocumentModel doc) {
300        VersioningOption option = fileManagerService.getVersioningOption();
301        if (option != null && option != VersioningOption.NONE) {
302            if (doc.isCheckedOut()) {
303                doc.checkIn(option, null);
304            }
305        }
306    }
307
308    /**
309     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
310     * behaviors from importers
311     */
312    @Deprecated
313    protected void checkInAfterAdd(DocumentModel doc) {
314        if (fileManagerService.doVersioningAfterAdd()) {
315            checkIn(doc);
316        }
317    }
318
319    protected void doSecurityCheck(CoreSession documentManager, String path, String typeName, TypeManager typeService)
320            throws DocumentSecurityException {
321        // perform the security checks
322        PathRef containerRef = new PathRef(path);
323        if (!documentManager.hasPermission(containerRef, READ_PROPERTIES)
324                || !documentManager.hasPermission(containerRef, ADD_CHILDREN)) {
325            throw new DocumentSecurityException("Not enough rights to create folder");
326        }
327        DocumentModel container = documentManager.getDocument(containerRef);
328
329        Type containerType = typeService.getType(container.getType());
330        if (containerType == null) {
331            return;
332        }
333
334        if (!typeService.isAllowedSubType(typeName, container.getType(), container)) {
335            throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s",
336                    typeName, containerType.getId()));
337        }
338    }
339
340}