private static Comment makeNewComment(Resource target, User sender, String body) throws IssueNotFound, PostingNotFound { Comment comment; Long id = Long.valueOf(target.getId()); switch (target.getType()) { case ISSUE_POST: Issue issue = Issue.finder.byId(id); if (issue == null) { throw new IssueNotFound(id); } comment = new IssueComment(issue, sender, body); break; case BOARD_POST: Posting posting = Posting.finder.byId(id); if (posting == null) { throw new PostingNotFound(id); } comment = new PostingComment(posting, sender, body); break; default: throw new IllegalArgumentException("Unsupported resource type: " + target.getType()); } return comment; }
/** * 업로드된 {@code file}을 주어진 {@code name}으로 {@code container}에 첨부한다. * * <p>when: 업로드된 파일이 사용자에게 첨부될 때. 혹은 사용자를 거치지 않고 바로 다른 리소스로 첨부될 때. * * <p>업로드된 파일을 업로드 디렉토리로 옮긴다. 이 때 파일이름을 그 파일의 해시값으로 변경한다. 그 후 이 파일에 대한 메타정보 및 첨부될 대상에 대한 정보를 이 * 엔터티에 담는다. 만약 이 엔터티와 같은 내용을 갖고 있는 엔터티가 이미 존재한다면, 이미 {@code container}에 같은 첨부가 존재하고 있으므로 첨부하지 않고 * {@code false}를 반환한다. 그렇지 않다면 첨부 후 {@code true}를 반환한다. * * @param file 첨부할 파일 * @param name 파일 이름 * @param container 파일이 첨부될 리소스 * @return 파일이 새로 첨부되었다면 {@code true}, 이미 같은 첨부가 존재하여 첨부되지 않았다면 {@code false} * @throws IOException * @throws NoSuchAlgorithmException */ @Transient public boolean store(File file, String name, Resource container) throws IOException, NoSuchAlgorithmException { // Store the file as its SHA1 hash in filesystem, and record its // metadata - containerType, containerId, size and hash - in Database. this.containerType = container.getType(); this.containerId = container.getId(); this.createdDate = JodaDateUtil.now(); if (name == null) { this.name = file.getName(); } else { this.name = name; } if (this.mimeType == null) { this.mimeType = FileUtil.detectMediaType(file, name); } // the size must be set before it is moved. this.size = file.length(); this.hash = Attachment.moveFileIntoUploadDirectory(file); // Add the attachment into the Database only if there is no same record. Attachment sameAttach = Attachment.findBy(this); if (sameAttach == null) { super.save(); return true; } else { this.id = sameAttach.id; return false; } }
/** * Create a comment from the given email. * * @param message * @param target * @throws MessagingException * @throws MailHandlerException * @throws IOException * @throws NoSuchAlgorithmException */ @Transactional public static Comment saveComment(IMAPMessage message, Resource target) throws MessagingException, MailHandlerException, IOException, NoSuchAlgorithmException { User author = IMAPMessageUtil.extractSender(message); if (!AccessControl.isProjectResourceCreatable(author, target.getProject(), target.getType())) { throw new PermissionDenied( cannotCreateMessage(author, target.getProject(), target.getType())); } Content parsedMessage = extractContent(message); Comment comment = makeNewComment(target, author, parsedMessage.body); comment.save(); Map<String, Attachment> relatedAttachments = saveAttachments(parsedMessage.attachments, comment.asResource()); if (new ContentType(parsedMessage.type).match(MimeType.HTML)) { // replace cid with attachments comment.contents = replaceCidWithAttachments(comment.contents, relatedAttachments); comment.update(); } new OriginalEmail(message.getMessageID(), comment.asResource()).save(); // Add the event addEvent(NotificationEvent.forNewComment(comment, author), message.getAllRecipients(), author); return comment; }
@Transactional protected static ReviewComment saveReviewComment( Resource target, User sender, Content content, String messageID, Address[] allRecipients) throws MessagingException, IOException, NoSuchAlgorithmException { ReviewComment comment; CommentThread thread = CommentThread.find.byId(Long.valueOf(target.getId())); if (thread == null) { throw new IllegalArgumentException(); } comment = new ReviewComment(); comment.setContents(content.body); comment.author = new UserIdent(sender); comment.thread = thread; comment.save(); Map<String, Attachment> relatedAttachments = saveAttachments(content.attachments, comment.asResource()); if (new ContentType(content.type).match(MimeType.HTML)) { // replace cid with attachments comment.setContents(replaceCidWithAttachments(comment.getContents(), relatedAttachments)); comment.update(); } new OriginalEmail(messageID, comment.asResource()).save(); // Add the event if (thread.isOnPullRequest()) { addEvent( NotificationEvent.forNewComment(sender, thread.pullRequest, comment), allRecipients, sender); } else { try { String commitId; if (thread instanceof CodeCommentThread) { commitId = ((CodeCommentThread) thread).commitId; } else if (thread instanceof NonRangedCodeCommentThread) { commitId = ((NonRangedCodeCommentThread) thread).commitId; } else { throw new IllegalArgumentException(); } addEvent( NotificationEvent.forNewCommitComment(target.getProject(), comment, commitId, sender), allRecipients, sender); } catch (Exception e) { Logger.warn("Failed to send a notification", e); } } return comment; }
/** * {@code user}가 {@code project}의 {@code resource}에 대해 저자로서의 수정 권한을 갖는지의 여부를 반환한다. * * <p>현재는 이슈 및 게시물과 그것들의 댓글에 대해서만 동작한다. * * @param user * @param project * @param resource * @return {@code user}가 {@code project}의 {@code resource}에 대해 저자로서의 수정 권한을 갖는지의 여부를 반환한다. */ private static boolean isEditableAsAuthor(User user, Project project, Resource resource) { switch (resource.getType()) { case ISSUE_POST: case ISSUE_COMMENT: case NONISSUE_COMMENT: case BOARD_POST: case CODE_COMMENT: return resource.getAuthorId().equals(user.id); default: return false; } }
/** * {@code user}가 프로젝트 리소스인 {@code resource}에 {@code operation}을 하는 것이 허용되는지의 여부를 반환한다. * * <p>See docs/technical/access-control.md for more information. * * @param user * @param project * @param resource * @param operation * @return */ private static boolean isProjectResourceAllowed( User user, Project project, Resource resource, Operation operation) { if (ProjectUser.isManager(user.id, project.id)) { return true; } // If the resource is an attachment, the permission depends on its container. if (resource.getType() == ResourceType.ATTACHMENT) { switch (operation) { case READ: return isAllowed(user, resource.getContainer(), Operation.READ); case UPDATE: case DELETE: return isAllowed(user, resource.getContainer(), Operation.UPDATE); } } // Access Control for members, nonmembers and anonymous. // - Anyone can read public project's resource. // - Members can update anything and delete anything except code repository. // - Nonmember can update or delete a resource if only // * the user is the author of the resource, // * the resource is not a code repository, // * and the project to which the resource belongs is public. // See docs/technical/access-control.md for more information. switch (operation) { case READ: return project.isPublic || ProjectUser.isMember(user.id, project.id); case UPDATE: if (ProjectUser.isMember(user.id, project.id)) { return true; } if (resource.getType() == ResourceType.CODE) { // Nonmember cannot update the repository. return false; } else { return project.isPublic && isEditableAsAuthor(user, project, resource); } case DELETE: if (resource.getType() == ResourceType.CODE) { return false; } else { return ProjectUser.isMember(user.id, project.id) || (project.isPublic && isEditableAsAuthor(user, project, resource)); } default: // undefined return false; } }
/** * Global 리소스에 대해 주어진 리소스의 operation을 허용하는지 여부 * * <p>임시 업로드 파일은 해당 파일을 업로드한 사용자만 접근할 수 있다. 비공개 프로젝트는 해당 프로젝트의 멤버만 접근할 수 있다. 공개 프로젝트는 모든 사용자가 접근할 * 수 있다. 사용자 및 사용자의 아바타는 그 사용자 본인만 갱신 혹은 삭제할 수 있다. 프로젝트는 그 프로젝트의 관리자만이 갱신 혹은 삭제할 수 있다. * * @param user * @param resource * @param operation * @return */ private static boolean isGlobalResourceAllowed( User user, Resource resource, Operation operation) { // Temporary attachments are allowed only for the user who uploads them. if (resource.getType() == ResourceType.ATTACHMENT && resource.getContainer().getType() == ResourceType.USER) { return user.id.equals(resource.getContainer().getId()); } if (operation == Operation.READ) { if (resource.getType() == ResourceType.PROJECT) { Project project = Project.find.byId(resource.getId()); return project != null && (project.isPublic || ProjectUser.isMember(user.id, project.id)); } // anyone can read any resource which is not a project. return true; } // UPDATE, DELETE switch (resource.getType()) { case USER: case USER_AVATAR: return user.id.equals(resource.getId()); case PROJECT: return ProjectUser.isManager(user.id, resource.getId()); default: // undefined return false; } }
/** * {@code from}에 첨부된 파일중 파일 아이디가{@code selectedFileIds}에 해당하는 첨부 파일을 {@code to}로 옮긴다. * * <p>when: 업로드 직후 일시적으로 사용자에게 첨부되었던 첨부 파일들을, 특정 리소스(이슈, 게시물 등)으로 옮기려 할 때 * * @param from 첨부 파일이 원래 있었던 리소스 * @param to 첨부 파일이 새로 옮겨질 리소스 * @return */ public static int moveOnlySelected(Resource from, Resource to, String[] selectedFileIds) { if (selectedFileIds.length == 0) { return NOTHING_TO_ATTACH; } List<Attachment> attachments = Attachment.find.where().idIn(Arrays.asList(selectedFileIds)).findList(); for (Attachment attachment : attachments) { if (attachment.containerId.equals(from.getId()) && attachment.containerType == from.getType()) { attachment.moveTo(to); } } return attachments.size(); }
/** * Create a review comment from the given email. * * @param message * @param target * @throws IOException * @throws MessagingException * @throws PermissionDenied * @throws NoSuchAlgorithmException */ static void saveReviewComment(IMAPMessage message, Resource target) throws IOException, MessagingException, PermissionDenied, NoSuchAlgorithmException { User sender = IMAPMessageUtil.extractSender(message); if (!AccessControl.isProjectResourceCreatable( sender, target.getProject(), ResourceType.REVIEW_COMMENT)) { throw new PermissionDenied( cannotCreateMessage(sender, target.getProject(), target.getType())); } Content content = extractContent(message); String messageID = message.getMessageID(); Address[] allRecipients = message.getAllRecipients(); saveReviewComment(target, sender, content, messageID, allRecipients); }
/** * {@code user}가 {@code resource}에 {@code operation}을 하는 것이 허용되는지의 여부를 반환한다. * * @param user * @param resource * @param operation * @return {@code user}가 {@code resource}에 {@code operation}을 하는 것이 허용되는지의 여부 */ public static boolean isAllowed(User user, Resource resource, Operation operation) { if (user.isSiteManager()) { return true; } Project project = resource.getProject(); if (project == null) { return isGlobalResourceAllowed(user, resource, operation); } else { return isProjectResourceAllowed(user, project, resource, operation); } }
/** * 이 객체를 리소스로 반환한다. * * <p>when: 권한검사시 사용 * * @return 리소스 */ @Override public Resource asResource() { boolean isContainerProject = containerType.equals(ResourceType.PROJECT); final Project project; final Resource container; if (isContainerProject) { project = Project.find.byId(Long.parseLong(containerId)); if (project == null) { throw new RuntimeException(messageForLosingProject()); } container = project.asResource(); } else { container = Resource.get(containerType, containerId); if (!(container instanceof GlobalResource)) { project = container.getProject(); if (project == null) { throw new RuntimeException(messageForLosingProject()); } } else { project = null; } } if (project != null) { return new Resource() { @Override public String getId() { return id.toString(); } @Override public Project getProject() { return project; } @Override public ResourceType getType() { return ResourceType.ATTACHMENT; } @Override public Resource getContainer() { return container; } }; } else { return new GlobalResource() { @Override public String getId() { return id.toString(); } @Override public ResourceType getType() { return ResourceType.ATTACHMENT; } @Override public Resource getContainer() { return container; } }; } }
/** * 이 첨부 파일을 {@code to}로 옮긴다. * * @param to 첨부 파일이 새로 옮겨질 리소스 * @return */ public void moveTo(Resource to) { containerType = to.getType(); containerId = to.getId(); update(); }
/** * 주어진 {@code container}에 첨부된 첨부 파일의 갯수를 반환한다. * * <p>when: * * @param container 첨부 파일이 첨부된 리소스 * @return 첨부 파일의 목록 */ public static int countByContainer(Resource container) { return find.where() .eq("containerType", container.getType()) .eq("containerId", container.getId()) .findRowCount(); }
/** * 주어진 {@code container}의 첨부된 첨부 파일의 목록을 반환한다. * * @param container 첨부 파일이 첨부된 리소스 * @return 첨부 파일의 목록 */ public static List<Attachment> findByContainer(Resource container) { return findByContainer(container.getType(), container.getId()); }