001/*
002 * (C) Copyright 2006-2013 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 *     Thierry Delprat
018 *     Gagnavarslan ehf
019 *     Florent Guillaume
020 *     Benoit Delbosc
021 *     Thierry Martins
022 */
023package org.nuxeo.ecm.webdav.backend;
024
025import java.io.IOException;
026import java.io.UnsupportedEncodingException;
027import java.net.URLEncoder;
028import java.security.Principal;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.LinkedList;
032import java.util.List;
033
034import org.apache.commons.io.FilenameUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.common.utils.Path;
039import org.nuxeo.ecm.core.api.Blob;
040import org.nuxeo.ecm.core.api.Blobs;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.DocumentNotFoundException;
044import org.nuxeo.ecm.core.api.DocumentRef;
045import org.nuxeo.ecm.core.api.Lock;
046import org.nuxeo.ecm.core.api.NuxeoException;
047import org.nuxeo.ecm.core.api.PathRef;
048import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
049import org.nuxeo.ecm.core.api.security.SecurityConstants;
050import org.nuxeo.ecm.core.api.trash.TrashService;
051import org.nuxeo.ecm.core.schema.FacetNames;
052import org.nuxeo.ecm.platform.filemanager.api.FileImporterContext;
053import org.nuxeo.ecm.platform.filemanager.api.FileManager;
054import org.nuxeo.ecm.webdav.EscapeUtils;
055import org.nuxeo.ecm.webdav.resource.ExistingResource;
056import org.nuxeo.runtime.api.Framework;
057
058public class SimpleBackend extends AbstractCoreBackend {
059
060    private static final Log log = LogFactory.getLog(SimpleBackend.class);
061
062    public static final String SOURCE_EDIT_KEYWORD = "source-edit";
063
064    public static final String ALWAYS_CREATE_FILE_PROP = "nuxeo.webdav.always-create-file";
065
066    public static final String TMP_EXTENSION = "tmp";
067
068    public static final String MSOFFICE_TMP_PREFIX = "~$";
069
070    protected String backendDisplayName;
071
072    protected String rootPath;
073
074    protected String rootUrl;
075
076    protected TrashService trashService;
077
078    protected LinkedList<String> orderedBackendNames;
079
080    protected SimpleBackend(String backendDisplayName, String rootPath, String rootUrl, CoreSession session) {
081        super(session);
082        this.backendDisplayName = backendDisplayName;
083        this.rootPath = rootPath;
084        this.rootUrl = rootUrl;
085    }
086
087    @Override
088    public String getRootPath() {
089        return rootPath;
090    }
091
092    @Override
093    public String getRootUrl() {
094        return rootUrl;
095    }
096
097    @Override
098    public String getBackendDisplayName() {
099        return backendDisplayName;
100    }
101
102    @Override
103    public boolean exists(String location) {
104        try {
105            DocumentModel doc = resolveLocation(location);
106            if (doc != null && !isTrashDocument(doc)) {
107                return true;
108            } else {
109                return false;
110            }
111        } catch (DocumentNotFoundException e) {
112            return false;
113        }
114    }
115
116    private boolean exists(DocumentRef ref) {
117        if (getSession().exists(ref)) {
118            DocumentModel model = getSession().getDocument(ref);
119            return !isTrashDocument(model);
120        }
121        return false;
122    }
123
124    @Override
125    public boolean hasPermission(DocumentRef docRef, String permission) {
126        return getSession().hasPermission(docRef, permission);
127    }
128
129    @Override
130    public DocumentModel updateDocument(DocumentModel doc, String name, Blob content) {
131        FileManager fileManager = Framework.getService(FileManager.class);
132        String parentPath = new Path(doc.getPathAsString()).removeLastSegments(1).toString();
133        try {
134            // this cannot be done before the update anymore
135            // doc.putContextData(SOURCE_EDIT_KEYWORD, "webdav");
136            FileImporterContext context = FileImporterContext.builder(session, content, parentPath)
137                                                             .overwrite(true)
138                                                             .fileName(name)
139                                                             .build();
140            doc = fileManager.createOrUpdateDocument(context);
141        } catch (IOException e) {
142            throw new NuxeoException("Error while updating document", e);
143        }
144        return doc;
145    }
146
147    @Override
148    public LinkedList<String> getVirtualFolderNames() {
149        if (orderedBackendNames == null) {
150            List<DocumentModel> children = getChildren(new PathRef(rootPath));
151            orderedBackendNames = new LinkedList<String>();
152            if (children != null) {
153                for (DocumentModel model : children) {
154                    orderedBackendNames.add(model.getName());
155                }
156            }
157        }
158        return orderedBackendNames;
159    }
160
161    @Override
162    public final boolean isVirtual() {
163        return false;
164    }
165
166    @Override
167    public boolean isRoot() {
168        return false;
169    }
170
171    @Override
172    public final Backend getBackend(String path) {
173        return this;
174    }
175
176    @Override
177    public DocumentModel resolveLocation(String location) {
178        Path resolvedLocation = parseLocation(location);
179
180        DocumentModel doc = null;
181        DocumentRef docRef = new PathRef(resolvedLocation.toString());
182        if (exists(docRef)) {
183            doc = getSession().getDocument(docRef);
184        } else {
185            String encodedPath = EscapeUtils.encodePath(resolvedLocation.toString());
186            if (!resolvedLocation.toString().equals(encodedPath)) {
187                DocumentRef encodedPathRef = new PathRef(encodedPath);
188                if (exists(encodedPathRef)) {
189                    doc = getSession().getDocument(encodedPathRef);
190                }
191            }
192
193            if (doc == null) {
194                String filename = resolvedLocation.lastSegment();
195                Path parentLocation = resolvedLocation.removeLastSegments(1);
196
197                // first try with spaces (for create New Folder)
198                String folderName = filename;
199                DocumentRef folderRef = new PathRef(parentLocation.append(folderName).toString());
200                if (exists(folderRef)) {
201                    doc = getSession().getDocument(folderRef);
202                }
203                // look for a child
204                DocumentModel parentDocument = resolveParent(parentLocation.toString());
205                if (parentDocument == null) {
206                    // parent doesn't exist, no use looking for a child
207                    return null;
208                }
209                List<DocumentModel> children = getChildren(parentDocument.getRef());
210                for (DocumentModel child : children) {
211                    BlobHolder bh = child.getAdapter(BlobHolder.class);
212                    if (bh != null) {
213                        Blob blob = bh.getBlob();
214                        if (blob != null) {
215                            try {
216                                String blobFilename = blob.getFilename();
217                                if (filename.equals(blobFilename)) {
218                                    doc = child;
219                                    break;
220                                } else if (EscapeUtils.encodePath(filename).equals(blobFilename)) {
221                                    doc = child;
222                                    break;
223                                } else if (URLEncoder.encode(filename, "UTF-8").equals(blobFilename)) {
224                                    doc = child;
225                                    break;
226                                } else if (encode(blobFilename.getBytes(), "ISO-8859-1").equals(filename)) {
227                                    doc = child;
228                                    break;
229                                }
230                            } catch (UnsupportedEncodingException e) {
231                                // cannot happen for UTF-8
232                                throw new RuntimeException(e);
233                            }
234                        }
235                    }
236                }
237            }
238        }
239        return doc;
240    }
241
242    protected DocumentModel resolveParent(String location) {
243        DocumentModel doc = null;
244        DocumentRef docRef = new PathRef(location.toString());
245        if (exists(docRef)) {
246            doc = getSession().getDocument(docRef);
247        } else {
248            Path locationPath = new Path(location);
249            String filename = locationPath.lastSegment();
250            Path parentLocation = locationPath.removeLastSegments(1);
251
252            // first try with spaces (for create New Folder)
253            String folderName = filename;
254            DocumentRef folderRef = new PathRef(parentLocation.append(folderName).toString());
255            if (exists(folderRef)) {
256                doc = getSession().getDocument(folderRef);
257            }
258        }
259        return doc;
260    }
261
262    @Override
263    public Path parseLocation(String location) {
264        Path finalLocation = new Path(rootPath);
265        Path rootUrlPath = new Path(rootUrl);
266        Path urlLocation = new Path(location);
267        Path cutLocation = urlLocation.removeFirstSegments(rootUrlPath.segmentCount());
268        finalLocation = finalLocation.append(cutLocation);
269        String fileName = finalLocation.lastSegment();
270        String parentPath = finalLocation.removeLastSegments(1).toString();
271        return new Path(parentPath).append(fileName);
272    }
273
274    @Override
275    public void removeItem(String location) {
276        DocumentModel docToRemove = resolveLocation(location);
277        if (docToRemove == null) {
278            throw new NuxeoException("Document path not found: " + location);
279        }
280        removeItem(docToRemove.getRef());
281    }
282
283    @Override
284    public void removeItem(DocumentRef ref) {
285        DocumentModel doc = getSession().getDocument(ref);
286        if (doc != null) {
287            if (isTemporaryFile(doc)) {
288                session.removeDocument(ref);
289            } else {
290                getTrashService().trashDocuments(Arrays.asList(doc));
291            }
292        } else {
293            log.warn("Can't move document " + ref.toString() + " to trash. Document did not found.");
294        }
295    }
296
297    protected boolean isTemporaryFile(DocumentModel doc) {
298        String name = doc.getName();
299        return FilenameUtils.getExtension(name).equalsIgnoreCase(TMP_EXTENSION) || name.startsWith(MSOFFICE_TMP_PREFIX);
300    }
301
302    @Override
303    public boolean isRename(String source, String destination) {
304        Path sourcePath = new Path(source);
305        Path destinationPath = new Path(destination);
306        return sourcePath.removeLastSegments(1).toString().equals(destinationPath.removeLastSegments(1).toString());
307    }
308
309    @Override
310    public void renameItem(DocumentModel source, String destinationName) {
311        source.putContextData(SOURCE_EDIT_KEYWORD, "webdav");
312        if (source.isFolder()) {
313            source.setPropertyValue("dc:title", destinationName);
314            moveItem(source, source.getParentRef(), destinationName);
315            source.putContextData("renameSource", "webdav");
316            getSession().saveDocument(source);
317        } else {
318            source.setPropertyValue("dc:title", destinationName);
319            BlobHolder bh = source.getAdapter(BlobHolder.class);
320            boolean blobUpdated = false;
321            if (bh != null) {
322                Blob blob = bh.getBlob();
323                if (blob != null) {
324                    blob.setFilename(destinationName);
325                    // as the name may have changed, reset the mime type so that the correct one will be computed
326                    blob.setMimeType(null);
327                    blobUpdated = true;
328                    bh.setBlob(blob);
329                    getSession().saveDocument(source);
330                }
331            }
332            if (!blobUpdated) {
333                source.setPropertyValue("dc:title", destinationName);
334                moveItem(source, source.getParentRef(), destinationName);
335                getSession().saveDocument(source);
336            }
337        }
338    }
339
340    @Override
341    public DocumentModel moveItem(DocumentModel source, PathRef targetParentRef) {
342        return moveItem(source, targetParentRef, source.getName());
343    }
344
345    @Override
346    public DocumentModel moveItem(DocumentModel source, DocumentRef targetParentRef, String name) {
347        cleanTrashPath(targetParentRef, name);
348        source.setPropertyValue("dc:title", name);
349        BlobHolder blobHolder = source.getAdapter(BlobHolder.class);
350        if (blobHolder != null) {
351            Blob blob = blobHolder.getBlob();
352            if (blob != null) {
353                blob.setFilename(name);
354                // as the name may have changed, reset the mime type so that the correct one will be computed
355                blob.setMimeType(null);
356                blobHolder.setBlob(blob);
357            }
358        }
359        source = getSession().saveDocument(source);
360        DocumentModel model = getSession().move(source.getRef(), targetParentRef, name);
361        return model;
362    }
363
364    @Override
365    public DocumentModel copyItem(DocumentModel source, PathRef targetParentRef) {
366        DocumentModel model = getSession().copy(source.getRef(), targetParentRef, source.getName());
367        return model;
368    }
369
370    @Override
371    public DocumentModel createFolder(String parentPath, String name) {
372        DocumentModel parent = resolveLocation(parentPath);
373        if (!parent.isFolder()) {
374            throw new NuxeoException("Can not create a child in a non folderish node");
375        }
376
377        String targetType = "Folder";
378        if ("WorkspaceRoot".equals(parent.getType())) {
379            targetType = "Workspace";
380        }
381        // name = cleanName(name);
382        cleanTrashPath(parent, name);
383        DocumentModel folder = getSession().createDocumentModel(parent.getPathAsString(), name, targetType);
384        folder.setPropertyValue("dc:title", name);
385        folder = getSession().createDocument(folder);
386        return folder;
387    }
388
389    @Override
390    public DocumentModel createFile(String parentPath, String name, Blob content) {
391        DocumentModel parent = resolveLocation(parentPath);
392        if (!parent.isFolder()) {
393            throw new NuxeoException("Can not create a child in a non folderish node");
394        }
395        try {
396            cleanTrashPath(parent, name);
397            DocumentModel file;
398            if (Framework.isBooleanPropertyTrue(ALWAYS_CREATE_FILE_PROP)) {
399                // compat for older versions, always create a File
400                file = getSession().createDocumentModel(parent.getPathAsString(), name, "File");
401                file.setPropertyValue("dc:title", name);
402                if (content != null) {
403                    BlobHolder bh = file.getAdapter(BlobHolder.class);
404                    if (bh != null) {
405                        bh.setBlob(content);
406                    }
407                }
408                file = getSession().createDocument(file);
409            } else {
410                // use the FileManager to create the file
411                FileImporterContext context = FileImporterContext.builder(getSession(), content,
412                        parent.getPathAsString()).fileName(name).build();
413                file = Framework.getService(FileManager.class).createOrUpdateDocument(context);
414            }
415            return file;
416        } catch (IOException e) {
417            throw new NuxeoException("Error child creating new folder", e);
418        }
419    }
420
421    @Override
422    public DocumentModel createFile(String parentPath, String name) {
423        Blob blob = Blobs.createBlob("", "application/octet-stream");
424        return createFile(parentPath, name, blob);
425    }
426
427    @Override
428    public String getDisplayName(DocumentModel doc) {
429        if (doc.isFolder()) {
430            return doc.getName();
431        } else {
432            String fileName = getFileName(doc);
433            if (fileName == null) {
434                fileName = doc.getName();
435            }
436            return fileName;
437        }
438    }
439
440    @Override
441    public List<DocumentModel> getChildren(DocumentRef ref) {
442        List<DocumentModel> result = new ArrayList<DocumentModel>();
443        List<DocumentModel> children = getSession(true).getChildren(ref);
444        for (DocumentModel child : children) {
445            if (child.hasFacet(FacetNames.HIDDEN_IN_NAVIGATION)) {
446                continue;
447            }
448            if (child.isTrashed()) {
449                continue;
450            }
451            if (!child.hasSchema("dublincore")) {
452                continue;
453            }
454            if (child.hasFacet(FacetNames.FOLDERISH) || child.getAdapter(BlobHolder.class) != null) {
455                result.add(child);
456            }
457        }
458        return result;
459    }
460
461    @Override
462    public boolean isLocked(DocumentRef ref) {
463        Lock lock = getSession().getLockInfo(ref);
464        return lock != null;
465    }
466
467    @Override
468    public boolean canUnlock(DocumentRef ref) {
469        Principal principal = getSession().getPrincipal();
470        if (principal == null || StringUtils.isEmpty(principal.getName())) {
471            log.error("Empty session principal. Error while canUnlock check.");
472            return false;
473        }
474        String checkoutUser = getCheckoutUser(ref);
475        return principal.getName().equals(checkoutUser);
476    }
477
478    @Override
479    public String lock(DocumentRef ref) {
480        if (getSession().hasPermission(ref, SecurityConstants.WRITE_PROPERTIES)) {
481            Lock lock = getSession().setLock(ref);
482            return lock.getOwner();
483        }
484        return ExistingResource.READONLY_TOKEN;
485    }
486
487    @Override
488    public boolean unlock(DocumentRef ref) {
489        if (!canUnlock(ref)) {
490            return false;
491        }
492        getSession().removeLock(ref);
493        return true;
494    }
495
496    @Override
497    public String getCheckoutUser(DocumentRef ref) {
498        Lock lock = getSession().getLockInfo(ref);
499        if (lock != null) {
500            return lock.getOwner();
501        }
502        return null;
503    }
504
505    @Override
506    public String getVirtualPath(String path) {
507        if (path.startsWith(this.rootPath)) {
508            return rootUrl + path.substring(this.rootPath.length());
509        } else {
510            return null;
511        }
512    }
513
514    @Override
515    public DocumentModel getDocument(String location) {
516        return resolveLocation(location);
517    }
518
519    protected String getFileName(DocumentModel doc) {
520        BlobHolder bh = doc.getAdapter(BlobHolder.class);
521        if (bh != null) {
522            Blob blob = bh.getBlob();
523            if (blob != null) {
524                return blob.getFilename();
525            }
526        }
527        return null;
528    }
529
530    protected boolean isTrashDocument(DocumentModel model) {
531        return model == null || model.isTrashed();
532    }
533
534    protected TrashService getTrashService() {
535        if (trashService == null) {
536            trashService = Framework.getService(TrashService.class);
537        }
538        return trashService;
539    }
540
541    protected boolean cleanTrashPath(DocumentModel parent, String name) {
542        Path checkedPath = new Path(parent.getPathAsString()).append(name);
543        if (getSession().exists(new PathRef(checkedPath.toString()))) {
544            DocumentModel model = getSession().getDocument(new PathRef(checkedPath.toString()));
545            if (model != null && model.isTrashed()) {
546                name = name + "." + System.currentTimeMillis();
547                getSession().move(model.getRef(), parent.getRef(), name);
548                return true;
549            }
550        }
551        return false;
552    }
553
554    protected boolean cleanTrashPath(DocumentRef parentRef, String name) {
555        DocumentModel parent = getSession().getDocument(parentRef);
556        return cleanTrashPath(parent, name);
557    }
558
559    protected String encode(byte[] bytes, String encoding) {
560        try {
561            return new String(bytes, encoding);
562        } catch (UnsupportedEncodingException e) {
563            throw new NuxeoException("Unsupported encoding " + encoding);
564        }
565    }
566
567}