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