001package de.deepamehta.plugins.files; 002 003import de.deepamehta.plugins.files.event.CheckDiskQuotaListener; 004import de.deepamehta.plugins.config.ConfigDefinition; 005import de.deepamehta.plugins.config.ConfigModificationRole; 006import de.deepamehta.plugins.config.ConfigService; 007import de.deepamehta.plugins.config.ConfigTarget; 008 009import de.deepamehta.core.Association; 010import de.deepamehta.core.DeepaMehtaObject; 011import de.deepamehta.core.Topic; 012import de.deepamehta.core.model.AssociationModel; 013import de.deepamehta.core.model.ChildTopicsModel; 014import de.deepamehta.core.model.SimpleValue; 015import de.deepamehta.core.model.TopicModel; 016import de.deepamehta.core.model.TopicRoleModel; 017import de.deepamehta.core.osgi.PluginActivator; 018import de.deepamehta.core.service.Cookies; 019import de.deepamehta.core.service.DeepaMehtaEvent; 020import de.deepamehta.core.service.EventListener; 021import de.deepamehta.core.service.Inject; 022import de.deepamehta.core.service.Transactional; 023import de.deepamehta.core.service.accesscontrol.AccessControl; 024import de.deepamehta.core.service.accesscontrol.Operation; 025import de.deepamehta.core.service.event.ResourceRequestFilterListener; 026import de.deepamehta.core.util.DeepaMehtaUtils; 027import de.deepamehta.core.util.JavaUtils; 028 029import org.apache.commons.io.IOUtils; 030 031import javax.ws.rs.Consumes; 032import javax.ws.rs.GET; 033import javax.ws.rs.Path; 034import javax.ws.rs.PathParam; 035import javax.ws.rs.POST; 036import javax.ws.rs.Produces; 037import javax.ws.rs.WebApplicationException; 038import javax.ws.rs.core.Response.Status; 039 040import javax.servlet.http.HttpServletRequest; 041 042import java.awt.Desktop; 043import java.io.FileOutputStream; 044import java.io.InputStream; 045import java.io.IOException; 046import java.io.File; 047import java.net.URL; 048import java.util.concurrent.Callable; 049import java.util.logging.Logger; 050import java.util.regex.Matcher; 051import java.util.regex.Pattern; 052 053 054 055@Path("/files") 056@Produces("application/json") 057public class FilesPlugin extends PluginActivator implements FilesService, ResourceRequestFilterListener, PathMapper { 058 059 // ------------------------------------------------------------------------------------------------------- Constants 060 061 public static final String FILE_REPOSITORY_PATH = System.getProperty("dm4.filerepo.path", "/"); 062 public static final boolean FILE_REPOSITORY_PER_WORKSPACE = Boolean.getBoolean("dm4.filerepo.per_workspace"); 063 public static final int DISK_QUOTA_MB = Integer.getInteger("dm4.filerepo.disk_quota", 150); 064 // Note: the default values are required in case no config file is in effect. This applies when DM is started 065 // via feature:install from Karaf. The default value must match the value defined in global POM. 066 067 private static final String FILE_REPOSITORY_URI = "/filerepo"; 068 069 private static final Pattern PER_WORKSPACE_PATH_PATTERN = Pattern.compile("/workspace-(\\d+).*"); 070 071 // Events 072 public static DeepaMehtaEvent CHECK_DISK_QUOTA = new DeepaMehtaEvent(CheckDiskQuotaListener.class) { 073 @Override 074 public void deliver(EventListener listener, Object... params) { 075 ((CheckDiskQuotaListener) listener).checkDiskQuota( 076 (String) params[0], (Long) params[1], (Long) params[2] 077 ); 078 } 079 }; 080 081 // ---------------------------------------------------------------------------------------------- Instance Variables 082 083 @Inject 084 private ConfigService configService; 085 086 private Logger logger = Logger.getLogger(getClass().getName()); 087 088 // -------------------------------------------------------------------------------------------------- Public Methods 089 090 091 092 // *********************************** 093 // *** FilesService Implementation *** 094 // *********************************** 095 096 097 098 // === File System Representation === 099 100 @GET 101 @Path("/file/{path}") 102 @Transactional 103 @Override 104 public Topic getFileTopic(@PathParam("path") String path) { 105 String operation = "Creating File topic for repository path \"" + path + "\""; 106 try { 107 logger.info(operation); 108 // 109 // 1) pre-checks 110 File file = absolutePath(path); // throws FileRepositoryException 111 checkExistence(file); // throws FileRepositoryException 112 // 113 // 2) check if topic already exists 114 Topic fileTopic = fetchFileTopic(file); 115 if (fileTopic != null) { 116 logger.info(operation + " ABORTED -- already exists"); 117 return fileTopic.loadChildTopics(); 118 } 119 // 3) create topic 120 return createFileTopic(file); 121 } catch (FileRepositoryException e) { 122 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 123 } catch (Exception e) { 124 throw new RuntimeException(operation + " failed", e); 125 } 126 } 127 128 @GET 129 @Path("/folder/{path}") 130 @Transactional 131 @Override 132 public Topic getFolderTopic(@PathParam("path") String path) { 133 String operation = "Creating Folder topic for repository path \"" + path + "\""; 134 try { 135 logger.info(operation); 136 // 137 // 1) pre-checks 138 File file = absolutePath(path); // throws FileRepositoryException 139 checkExistence(file); // throws FileRepositoryException 140 // 141 // 2) check if topic already exists 142 Topic folderTopic = fetchFolderTopic(file); 143 if (folderTopic != null) { 144 logger.info(operation + " ABORTED -- already exists"); 145 return folderTopic.loadChildTopics(); 146 } 147 // 3) create topic 148 return createFolderTopic(file); 149 } catch (FileRepositoryException e) { 150 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 151 } catch (Exception e) { 152 throw new RuntimeException(operation + " failed", e); 153 } 154 } 155 156 // --- 157 158 @GET 159 @Path("/parent/{id}/file/{path}") 160 @Transactional 161 @Override 162 public Topic getChildFileTopic(@PathParam("id") long folderTopicId, @PathParam("path") String path) { 163 Topic topic = getFileTopic(path); 164 createFolderAssociation(folderTopicId, topic); 165 return topic; 166 } 167 168 @GET 169 @Path("/parent/{id}/folder/{path}") 170 @Transactional 171 @Override 172 public Topic getChildFolderTopic(@PathParam("id") long folderTopicId, @PathParam("path") String path) { 173 Topic topic = getFolderTopic(path); 174 createFolderAssociation(folderTopicId, topic); 175 return topic; 176 } 177 178 179 180 // === File Repository === 181 182 @POST 183 @Path("/{path}") 184 @Consumes("multipart/form-data") 185 @Transactional 186 @Override 187 public StoredFile storeFile(UploadedFile file, @PathParam("path") String path) { 188 String operation = "Storing " + file + " at repository path \"" + path + "\""; 189 try { 190 logger.info(operation); 191 // 1) pre-checks 192 File directory = absolutePath(path); // throws FileRepositoryException 193 checkExistence(directory); // throws FileRepositoryException 194 // 195 // 2) store file 196 File repoFile = unusedPath(directory, file); 197 file.write(repoFile); 198 // 199 // 3) create topic 200 Topic fileTopic = createFileTopic(repoFile); 201 return new StoredFile(repoFile.getName(), fileTopic.getId()); 202 } catch (FileRepositoryException e) { 203 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 204 } catch (Exception e) { 205 throw new RuntimeException(operation + " failed", e); 206 } 207 } 208 209 // Note: this is not a resource method. So we don't throw a WebApplicationException here. 210 @Override 211 public Topic createFile(InputStream in, String path) { 212 String operation = "Creating file (from input stream) at repository path \"" + path + "\""; 213 try { 214 logger.info(operation); 215 // 1) pre-checks 216 File file = absolutePath(path); // throws FileRepositoryException 217 // 218 // 2) store file 219 FileOutputStream out = new FileOutputStream(file); 220 IOUtils.copy(in, out); 221 in.close(); 222 out.close(); 223 // 224 // 3) create topic 225 // ### TODO: think about overwriting an existing file. 226 // ### FIXME: in this case the existing file topic is not updated and might reflect e.g. the wrong size. 227 return getFileTopic(path); 228 } catch (Exception e) { 229 throw new RuntimeException(operation + " failed", e); 230 } 231 } 232 233 @POST 234 @Path("/{path}/folder/{folder_name}") 235 @Override 236 public void createFolder(@PathParam("folder_name") String folderName, @PathParam("path") String path) { 237 String operation = "Creating folder \"" + folderName + "\" at repository path \"" + path + "\""; 238 try { 239 logger.info(operation); 240 // 1) pre-checks 241 File directory = absolutePath(path); // throws FileRepositoryException 242 checkExistence(directory); // throws FileRepositoryException 243 // 244 // 2) create directory 245 File repoFile = path(directory, folderName); 246 if (repoFile.exists()) { 247 throw new RuntimeException("File or directory \"" + repoFile + "\" already exists"); 248 } 249 // 250 boolean success = repoFile.mkdir(); 251 // 252 if (!success) { 253 throw new RuntimeException("File.mkdir() failed (file=\"" + repoFile + "\")"); 254 } 255 } catch (FileRepositoryException e) { 256 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 257 } catch (Exception e) { 258 throw new RuntimeException(operation + " failed", e); 259 } 260 } 261 262 // --- 263 264 @GET 265 @Path("/{path}/info") 266 @Override 267 public ResourceInfo getResourceInfo(@PathParam("path") String path) { 268 String operation = "Getting resource info for repository path \"" + path + "\""; 269 try { 270 logger.info(operation); 271 // 272 File file = absolutePath(path); // throws FileRepositoryException 273 checkExistence(file); // throws FileRepositoryException 274 // 275 return new ResourceInfo(file); 276 } catch (FileRepositoryException e) { 277 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 278 } catch (Exception e) { 279 throw new RuntimeException(operation + " failed", e); 280 } 281 } 282 283 @GET 284 @Path("/{path}") 285 @Override 286 public DirectoryListing getDirectoryListing(@PathParam("path") String path) { 287 String operation = "Getting directory listing for repository path \"" + path + "\""; 288 try { 289 logger.info(operation); 290 // 291 File directory = absolutePath(path); // throws FileRepositoryException 292 checkExistence(directory); // throws FileRepositoryException 293 // 294 return new DirectoryListing(directory, this); 295 // ### TODO: if directory is no directory send NOT FOUND 296 } catch (FileRepositoryException e) { 297 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 298 } catch (Exception e) { 299 throw new RuntimeException(operation + " failed", e); 300 } 301 } 302 303 @Override 304 public String getRepositoryPath(URL url) { 305 String operation = "Checking for file repository URL (\"" + url + "\")"; 306 try { 307 if (!DeepaMehtaUtils.isDeepaMehtaURL(url)) { 308 logger.info(operation + " => null"); 309 return null; 310 } 311 // 312 String path = url.getPath(); 313 if (!path.startsWith(FILE_REPOSITORY_URI)) { 314 logger.info(operation + " => null"); 315 return null; 316 } 317 // 318 String repoPath = path.substring(FILE_REPOSITORY_URI.length()); 319 logger.info(operation + " => \"" + repoPath + "\""); 320 return repoPath; 321 } catch (Exception e) { 322 throw new RuntimeException(operation + " failed", e); 323 } 324 } 325 326 // --- 327 328 // Note: this is not a resource method. So we don't throw a WebApplicationException here. 329 // To access a file remotely use the /filerepo resource. 330 @Override 331 public File getFile(String path) { 332 String operation = "Accessing the file at \"" + path + "\""; 333 try { 334 logger.info(operation); 335 // 336 File file = absolutePath(path); // throws FileRepositoryException 337 checkExistence(file); // throws FileRepositoryException 338 return file; 339 } catch (Exception e) { 340 throw new RuntimeException(operation + " failed", e); 341 } 342 } 343 344 // Note: this is not a resource method. So we don't throw a WebApplicationException here. 345 // To access a file remotely use the /filerepo resource. 346 @Override 347 public File getFile(long fileTopicId) { 348 String operation = "Accessing the file of File topic " + fileTopicId; 349 try { 350 logger.info(operation); 351 // 352 String path = repoPath(fileTopicId); 353 File file = absolutePath(path); // throws FileRepositoryException 354 checkExistence(file); // throws FileRepositoryException 355 return file; 356 } catch (Exception e) { 357 throw new RuntimeException(operation + " failed", e); 358 } 359 } 360 361 // --- 362 363 @POST 364 @Path("/open/{id}") 365 @Override 366 public void openFile(@PathParam("id") long fileTopicId) { 367 String operation = "Opening the file of File topic " + fileTopicId; 368 try { 369 logger.info(operation); 370 // 371 String path = repoPath(fileTopicId); 372 File file = absolutePath(path); // throws FileRepositoryException 373 checkExistence(file); // throws FileRepositoryException 374 // 375 logger.info("### Opening file \"" + file + "\""); 376 Desktop.getDesktop().open(file); 377 } catch (FileRepositoryException e) { 378 throw new WebApplicationException(new RuntimeException(operation + " failed", e), e.getStatus()); 379 } catch (Exception e) { 380 throw new RuntimeException(operation + " failed", e); 381 } 382 } 383 384 385 386 // **************************** 387 // *** Hook Implementations *** 388 // **************************** 389 390 391 392 @Override 393 public void preInstall() { 394 configService.registerConfigDefinition(new ConfigDefinition( 395 ConfigTarget.TYPE_INSTANCES, "dm4.accesscontrol.username", 396 new TopicModel("dm4.files.disk_quota", new SimpleValue(DISK_QUOTA_MB)), 397 ConfigModificationRole.ADMIN 398 )); 399 } 400 401 @Override 402 public void init() { 403 publishFileSystem(FILE_REPOSITORY_URI, FILE_REPOSITORY_PATH); 404 } 405 406 @Override 407 public void shutdown() { 408 // Note 1: unregistering is crucial e.g. for redeploying the Files plugin. The next register call 409 // (at preInstall() time) would fail as the Config service already holds such a registration. 410 // Note 2: we must check if the Config service is still available. If the Config plugin is redeployed the 411 // Files plugin is stopped/started as well but at shutdown() time the Config service is already gone. 412 if (configService != null) { 413 configService.unregisterConfigDefinition("dm4.files.disk_quota"); 414 } else { 415 logger.warning("Config service is already gone"); 416 } 417 } 418 419 420 421 // ******************************** 422 // *** Listener Implementations *** 423 // ******************************** 424 425 426 427 @Override 428 public void resourceRequestFilter(HttpServletRequest request) { 429 try { 430 String repoPath = repoPath(request); 431 if (repoPath != null) { 432 logger.info("### Checking access to repository path \"" + repoPath + "\""); 433 File path = absolutePath(repoPath); // throws FileRepositoryException 403 Forbidden 434 checkExistence(path); // throws FileRepositoryException 404 Not Found 435 checkAuthorization(path, repoPath, request); // throws FileRepositoryException 401 Unauthorized 436 } 437 } catch (FileRepositoryException e) { 438 throw new WebApplicationException(e, e.getStatus()); 439 } 440 } 441 442 443 444 // ********************************* 445 // *** PathMapper Implementation *** 446 // ********************************* 447 448 449 450 @Override 451 public String repoPath(File path) { 452 try { 453 String repoPath = path.getPath(); 454 // 455 if (!repoPath.startsWith(FILE_REPOSITORY_PATH)) { 456 throw new RuntimeException("Absolute path \"" + path + "\" is not a repository path"); 457 } 458 // 459 if (!FILE_REPOSITORY_PATH.equals("/")) { 460 repoPath = repoPath.substring(FILE_REPOSITORY_PATH.length()); 461 } 462 // ### FIXME: Windows drive letter? 463 return repoPath; 464 } catch (Exception e) { 465 throw new RuntimeException("Mapping absolute path \"" + path + "\" to a repository path failed", e); 466 } 467 } 468 469 470 471 472 // ------------------------------------------------------------------------------------------------- Private Methods 473 474 475 476 // === File System Representation === 477 478 /** 479 * Fetches the File topic representing the file at the given absolute path. 480 * If no such File topic exists <code>null</code> is returned. 481 * 482 * @param path An absolute path. 483 */ 484 private Topic fetchFileTopic(File path) { 485 return fetchTopic(path, "dm4.files.file"); 486 } 487 488 /** 489 * Fetches the Folder topic representing the folder at the given absolute path. 490 * If no such Folder topic exists <code>null</code> is returned. 491 * 492 * @param path An absolute path. 493 */ 494 private Topic fetchFolderTopic(File path) { 495 return fetchTopic(path, "dm4.files.folder"); 496 } 497 498 // --- 499 500 /** 501 * Fetches the File/Folder topic representing the file/directory at the given absolute path. 502 * If no such File/Folder topic exists <code>null</code> is returned. 503 * 504 * @param path An absolute path. 505 * @param topicTypeUri The type of the topic to fetch: either "dm4.files.file" or "dm4.files.folder". 506 */ 507 private Topic fetchTopic(File path, String topicTypeUri) { 508 String repoPath = repoPath(path); 509 Topic topic = dms.getTopic("dm4.files.path", new SimpleValue(repoPath)); 510 if (topic != null) { 511 return topic.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent", topicTypeUri); 512 // ### FIXME: had fetchComposite=true 513 } 514 return null; 515 } 516 517 // --- 518 519 /** 520 * Creates a File topic representing the file at the given absolute path. 521 * 522 * @param path An absolute path. 523 */ 524 private Topic createFileTopic(File path) throws Exception { 525 ChildTopicsModel childTopics = new ChildTopicsModel() 526 .put("dm4.files.file_name", path.getName()) 527 .put("dm4.files.path", repoPath(path)) 528 .put("dm4.files.size", path.length()); 529 // 530 String mediaType = JavaUtils.getFileType(path.getName()); 531 if (mediaType != null) { 532 childTopics.put("dm4.files.media_type", mediaType); 533 } 534 // 535 return createFileOrFolderTopic(new TopicModel("dm4.files.file", childTopics)); // throws Exception 536 } 537 538 /** 539 * Creates a Folder topic representing the directory at the given absolute path. 540 * 541 * @param path An absolute path. 542 */ 543 private Topic createFolderTopic(File path) throws Exception { 544 String folderName = path.getName(); 545 String repoPath = repoPath(path); 546 // 547 // root folder needs special treatment 548 if (repoPath.equals("/")) { 549 folderName = ""; 550 } 551 // 552 ChildTopicsModel childTopics = new ChildTopicsModel() 553 .put("dm4.files.folder_name", folderName) 554 .put("dm4.files.path", repoPath); 555 // 556 return createFileOrFolderTopic(new TopicModel("dm4.files.folder", childTopics)); // throws Exception 557 } 558 559 // --- 560 561 private Topic createFileOrFolderTopic(final TopicModel model) throws Exception { 562 // We suppress standard workspace assignment here as File and Folder topics require a special assignment 563 Topic topic = dms.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Topic>() { // throws Exception 564 @Override 565 public Topic call() { 566 return dms.createTopic(model); 567 } 568 }); 569 createWorkspaceAssignment(topic, repoPath(topic)); 570 return topic; 571 } 572 573 /** 574 * @param topic a File topic, or a Folder topic. 575 */ 576 private void createFolderAssociation(final long folderTopicId, Topic topic) { 577 try { 578 final long topicId = topic.getId(); 579 boolean exists = dms.getAssociations(folderTopicId, topicId, "dm4.core.aggregation").size() > 0; 580 if (!exists) { 581 // We suppress standard workspace assignment as the folder association requires a special assignment 582 Association assoc = dms.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Association>() { 583 @Override 584 public Association call() { 585 return dms.createAssociation(new AssociationModel("dm4.core.aggregation", 586 new TopicRoleModel(folderTopicId, "dm4.core.parent"), 587 new TopicRoleModel(topicId, "dm4.core.child") 588 )); 589 } 590 }); 591 createWorkspaceAssignment(assoc, repoPath(topic)); 592 } 593 } catch (Exception e) { 594 throw new RuntimeException("Creating association to Folder topic " + folderTopicId + " failed", e); 595 } 596 } 597 598 // --- 599 600 /** 601 * Creates a workspace assignment for a File topic, a Folder topic, or a folder association (type "Aggregation"). 602 * The workspce is calculated from both, the "dm4.filerepo.per_workspace" flag and the given repository path. 603 * 604 * @param object a File topic, a Folder topic, or a folder association (type "Aggregation"). 605 */ 606 private void createWorkspaceAssignment(DeepaMehtaObject object, String repoPath) { 607 try { 608 long workspaceId; 609 AccessControl ac = dms.getAccessControl(); 610 if (FILE_REPOSITORY_PER_WORKSPACE) { 611 Matcher m = PER_WORKSPACE_PATH_PATTERN.matcher(repoPath); 612 if (m.matches()) { 613 workspaceId = Long.parseLong(m.group(1)); 614 } else { 615 throw new RuntimeException("No workspace recognized in repository path \"" + repoPath + "\""); 616 } 617 } else { 618 workspaceId = ac.getDeepaMehtaWorkspaceId(); 619 } 620 // 621 ac.assignToWorkspace(object, workspaceId); 622 } catch (Exception e) { 623 throw new RuntimeException("Creating workspace assignment for File/Folder topic or folder association " + 624 "failed", e); 625 } 626 } 627 628 629 630 // === File Repository === 631 632 /** 633 * Maps a repository path to an absolute path. 634 * Checks the repository path to fight directory traversal attacks. 635 * 636 * @param repoPath A repository path. Relative to the repository base path. 637 * Must begin with slash, no slash at the end. 638 * 639 * @return The absolute path. 640 */ 641 private File absolutePath(String repoPath) throws FileRepositoryException { 642 try { 643 File repo = new File(FILE_REPOSITORY_PATH); 644 // 645 if (!repo.exists()) { 646 throw new RuntimeException("File repository \"" + repo + "\" does not exist"); 647 } 648 // 649 if (FILE_REPOSITORY_PER_WORKSPACE && repoPath.equals("/")) { 650 repo = new File(repo, "/workspace-" + getWorkspaceId()); 651 createWorkspaceFileRepository(repo); 652 } 653 // 654 repo = new File(repo, repoPath); 655 // 656 return checkPath(repo); // throws FileRepositoryException 403 Forbidden 657 } catch (FileRepositoryException e) { 658 throw e; 659 } catch (Exception e) { 660 throw new RuntimeException("Mapping repository path \"" + repoPath + "\" to an absolute path failed", e); 661 } 662 } 663 664 private long getWorkspaceId() { 665 Cookies cookies = Cookies.get(); 666 if (!cookies.has("dm4_workspace_id")) { 667 throw new RuntimeException("If \"dm4.filerepo.per_workspace\" is set the request requires a " + 668 "\"dm4_workspace_id\" cookie"); 669 } 670 return cookies.getLong("dm4_workspace_id"); 671 } 672 673 private void createWorkspaceFileRepository(File repo) { 674 try { 675 if (!repo.exists()) { 676 boolean created = repo.mkdir(); 677 if (created) { 678 logger.info("### Per-workspace file repository created: \"" + repo + "\""); 679 } else { 680 throw new RuntimeException("Directory \"" + repo + "\" not created successfully"); 681 } 682 } 683 } catch (Exception e) { 684 throw new RuntimeException("Creating per-workspace file repository failed", e); 685 } 686 } 687 688 // --- 689 690 /** 691 * Checks if the absolute path represents a directory traversal attack. 692 * If so a FileRepositoryException (403 Forbidden) is thrown. 693 * 694 * @param path The absolute path to check. 695 * 696 * @return The canonical form of the absolute path. 697 */ 698 private File checkPath(File path) throws FileRepositoryException, IOException { 699 // Note: a directory path returned by getCanonicalPath() never contains a "/" at the end. 700 // Thats why "dm4.filerepo.path" is expected to have no "/" at the end as well. 701 path = path.getCanonicalFile(); // throws IOException 702 boolean pointsToRepository = path.getPath().startsWith(FILE_REPOSITORY_PATH); 703 // 704 logger.info("Checking path \"" + path + "\"\n dm4.filerepo.path=" + 705 "\"" + FILE_REPOSITORY_PATH + "\" => " + (pointsToRepository ? "PATH OK" : "FORBIDDEN")); 706 // 707 if (!pointsToRepository) { 708 throw new FileRepositoryException("\"" + path + "\" does not point to file repository", Status.FORBIDDEN); 709 } 710 // 711 return path; 712 } 713 714 private void checkExistence(File path) throws FileRepositoryException { 715 boolean exists = path.exists(); 716 // 717 logger.info("Checking existence of \"" + path + "\" => " + (exists ? "EXISTS" : "NOT FOUND")); 718 // 719 if (!exists) { 720 throw new FileRepositoryException("File or directory \"" + path + "\" does not exist", Status.NOT_FOUND); 721 } 722 } 723 724 /** 725 * Checks if the user associated with a request is authorized to access a repository file. 726 * If not authorized a FileRepositoryException (401 Unauthorized) is thrown. 727 * 728 * @param path The absolute path of the file to check (as calculated already from the repository path). 729 * @param repoPath The repository path of the file to check. 730 * @param request The request. 731 */ 732 private void checkAuthorization(File path, String repoPath, HttpServletRequest request) 733 throws FileRepositoryException { 734 try { 735 if (FILE_REPOSITORY_PER_WORKSPACE) { 736 // We check authorization for the repository path by checking access to the corresponding File topic. 737 Topic fileTopic = fetchFileTopic(path); 738 if (fileTopic != null) { 739 // We must perform access control for the fetchFileTopic() call manually here. 740 // 741 // Although the AccessControlPlugin's PreGetTopicListener kicks in, the request is *not* injected 742 // into the AccessControlPlugin letting fetchFileTopic() effectively run as "System". 743 // 744 // Note: checkAuthorization() is called (indirectly) from an OSGi HTTP service static resource 745 // HttpContext. JAX-RS is not involved here. That's why no JAX-RS injection takes place. 746 String username = dms.getAccessControl().getUsername(request); 747 long fileTopicId = fileTopic.getId(); 748 if (!dms.getAccessControl().hasPermission(username, Operation.READ, fileTopicId)) { 749 throw new FileRepositoryException(userInfo(username) + " has no READ permission for " + 750 "repository path \"" + repoPath + "\" (File topic ID=" + fileTopicId + ")", 751 Status.UNAUTHORIZED); 752 } 753 } else { 754 throw new RuntimeException("Missing File topic for repository path \"" + repoPath + "\""); 755 } 756 } 757 } catch (FileRepositoryException e) { 758 throw e; 759 } catch (Exception e) { 760 throw new RuntimeException("Checking authorization for repository path \"" + repoPath + "\" failed", e); 761 } 762 } 763 764 private String userInfo(String username) { 765 return "user " + (username != null ? "\"" + username + "\"" : "<anonymous>"); 766 } 767 768 // --- 769 770 /** 771 * Constructs an absolute path from an absolute path and a file name. 772 * 773 * @param directory An absolute path. 774 * 775 * @return The constructed absolute path. 776 */ 777 private File path(File directory, String fileName) { 778 return new File(directory, fileName); 779 } 780 781 /** 782 * Constructs an absolute path for storing an uploaded file. 783 * If a file with that name already exists in the specified directory it remains untouched and the uploaded file 784 * is stored with a unique name (by adding a number). 785 * 786 * @param directory The directory to store the uploaded file to. 787 * An absolute path. 788 * 789 * @return The constructed absolute path. 790 */ 791 private File unusedPath(File directory, UploadedFile file) { 792 return JavaUtils.findUnusedFile(path(directory, file.getName())); 793 } 794 795 // --- 796 797 /** 798 * Returns the repository path of a File/Folder topic. 799 */ 800 private String repoPath(long fileTopicId) { 801 return repoPath(dms.getTopic(fileTopicId)); 802 } 803 804 private String repoPath(Topic topic) { 805 return topic.getChildTopics().getString("dm4.files.path"); 806 } 807 808 /** 809 * Returns the repository path of a filerepo request. 810 * 811 * @return The repository path or <code>null</code> if the request is not a filerepo request. 812 */ 813 public String repoPath(HttpServletRequest request) { 814 String repoPath = null; 815 String requestURI = request.getRequestURI(); 816 if (requestURI.startsWith(FILE_REPOSITORY_URI)) { 817 repoPath = requestURI.substring(FILE_REPOSITORY_URI.length()); 818 repoPath = JavaUtils.decodeURIComponent(repoPath); 819 } 820 return repoPath; 821 } 822}