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}