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}