/**
* Yobi, Project Hosting SW
*
* Copyright 2012 NAVER Corp.
* http://yobi.io
*
* @Author Sangcheol Hwang
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package controllers;
import actions.AnonymousCheckAction;
import actions.DefaultProjectCheckAction;
import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Junction;
import com.avaje.ebean.Page;
import controllers.annotation.IsAllowed;
import info.schleichardt.play2.mailplugin.Mailer;
import models.*;
import models.enumeration.Operation;
import models.enumeration.ProjectScope;
import models.enumeration.RequestState;
import models.enumeration.ResourceType;
import models.enumeration.RoleType;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.mail.HtmlEmail;
import org.codehaus.jackson.node.ObjectNode;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.tmatesoft.svn.core.SVNException;
import play.Logger;
import play.data.Form;
import play.data.validation.ValidationError;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Http.MultipartFormData.FilePart;
import play.mvc.Result;
import play.mvc.With;
import playRepository.Commit;
import playRepository.PlayRepository;
import playRepository.RepositoryService;
import scala.reflect.io.FileOperationException;
import utils.*;
import play.data.validation.Constraints.PatternValidator;
import validation.ExConstraints.RestrictedValidator;
import views.html.project.create;
import views.html.project.delete;
import views.html.project.home;
import views.html.project.setting;
import views.html.project.transfer;
import views.html.project.change_vcs;
import javax.servlet.ServletException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import static play.data.Form.form;
import static play.libs.Json.toJson;
import static utils.LogoUtil.*;
import static utils.TemplateHelper.*;
public class ProjectApp extends Controller {
private static final int ISSUE_MENTION_SHOW_LIMIT = 2000;
private static final int MAX_FETCH_PROJECTS = 1000;
private static final int COMMIT_HISTORY_PAGE = 0;
private static final int COMMIT_HISTORY_SHOW_LIMIT = 10;
private static final int RECENLTY_ISSUE_SHOW_LIMIT = 10;
private static final int RECENLTY_POSTING_SHOW_LIMIT = 10;
private static final int RECENT_PULL_REQUEST_SHOW_LIMIT = 10;
private static final int PROJECT_COUNT_PER_PAGE = 10;
private static final String HTML = "text/html";
private static final String JSON = "application/json";
@With(AnonymousCheckAction.class)
@IsAllowed(Operation.UPDATE)
public static Result projectOverviewUpdate(String ownerId, String projectName){
Project targetProject = Project.findByOwnerAndProjectName(ownerId, projectName);
if (targetProject == null) {
return notFound(ErrorViews.NotFound.render("error.notfound"));
}
targetProject.overview = request().body().asJson().findPath("overview").getTextValue();
targetProject.save();
ObjectNode result = Json.newObject();
result.put("overview", targetProject.overview);
return ok(result);
}
@IsAllowed(Operation.READ)
public static Result project(String ownerId, String projectName)
throws IOException, ServletException, SVNException, GitAPIException {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
List<History> histories = getProjectHistory(ownerId, project);
UserApp.currentUser().visits(project);
String tabId = StringUtils.defaultIfBlank(request().getQueryString("tabId"), "readme");
return ok(home.render(getTitleMessage(tabId), project, histories, tabId));
}
private static String getTitleMessage(String tabId) {
switch (tabId) {
case "history":
return "project.history.recent";
case "dashboard":
return "title.projectDashboard";
default:
case "readme":
return "title.projectHome";
}
}
private static List<History> getProjectHistory(String ownerId, Project project)
throws IOException, ServletException, SVNException, GitAPIException {
project.fixInvalidForkData();
PlayRepository repository = RepositoryService.getRepository(project);
List<Commit> commits = null;
try {
commits = repository.getHistory(COMMIT_HISTORY_PAGE, COMMIT_HISTORY_SHOW_LIMIT, null, null);
} catch (NoHeadException e) {
// NOOP
}
List<Issue> issues = Issue.findRecentlyCreated(project, RECENLTY_ISSUE_SHOW_LIMIT);
List<Posting> postings = Posting.findRecentlyCreated(project, RECENLTY_POSTING_SHOW_LIMIT);
List<PullRequest> pullRequests = PullRequest.findRecentlyReceived(project, RECENT_PULL_REQUEST_SHOW_LIMIT);
return History.makeHistory(ownerId, project, commits, issues, postings, pullRequests);
}
@With(AnonymousCheckAction.class)
public static Result newProjectForm() {
Form<Project> projectForm = form(Project.class).bindFromRequest("owner");
projectForm.discardErrors();
List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id);
return ok(create.render("title.newProject", projectForm, orgUserList));
}
@IsAllowed(Operation.UPDATE)
public static Result settingForm(String ownerId, String projectName) throws Exception {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
Form<Project> projectForm = form(Project.class).fill(project);
PlayRepository repository = RepositoryService.getRepository(project);
return ok(setting.render("title.projectSetting", projectForm, project, repository.getBranches()));
}
@Transactional
public static Result newProject() throws Exception {
Form<Project> filledNewProjectForm = form(Project.class).bindFromRequest();
String owner = filledNewProjectForm.field("owner").value();
User user = UserApp.currentUser();
Organization organization = Organization.findByName(owner);
if ((!AccessControl.isGlobalResourceCreatable(user))
|| (Organization.isNameExist(owner) && !OrganizationUser.isAdmin(organization.id, user.id))) {
return forbidden(ErrorViews.Forbidden.render("'" + user.name + "' has no permission"));
}
if (validateWhenNew(filledNewProjectForm)) {
return badRequest(create.render("title.newProject",
filledNewProjectForm, OrganizationUser.findByAdmin(user.id)));
}
Project project = filledNewProjectForm.get();
if (Organization.isNameExist(owner)) {
project.organization = organization;
}
ProjectUser.assignRole(user.id, Project.create(project), RoleType.MANAGER);
RepositoryService.createRepository(project);
saveProjectMenuSetting(project);
return redirect(routes.ProjectApp.project(project.owner, project.name));
}
private static boolean validateWhenNew(Form<Project> newProjectForm) {
String owner = newProjectForm.field("owner").value();
String name = newProjectForm.field("name").value();
User user = User.findByLoginId(owner);
boolean ownerIsUser = User.isLoginIdExist(owner);
boolean ownerIsOrganization = Organization.isNameExist(owner);
if (!ownerIsUser && !ownerIsOrganization) {
newProjectForm.reject("owner", "project.owner.invalidate");
}
if (ownerIsUser && !UserApp.currentUser().id.equals(user.id)) {
newProjectForm.reject("owner", "project.owner.invalidate");
}
if (Project.exists(owner, name)) {
newProjectForm.reject("name", "project.name.duplicate");
}
ValidationError error = newProjectForm.error("name");
if (error != null) {
if (PatternValidator.message.equals(error.message())) {
newProjectForm.errors().remove("name");
newProjectForm.reject("name", "project.name.alert");
} else if (RestrictedValidator.message.equals(error.message())) {
newProjectForm.errors().remove("name");
newProjectForm.reject("name", "project.name.reserved.alert");
}
}
return newProjectForm.hasErrors();
}
@Transactional
@IsAllowed(Operation.UPDATE)
public static Result settingProject(String ownerId, String projectName)
throws IOException, NoSuchAlgorithmException, UnsupportedOperationException, ServletException {
Form<Project> filledUpdatedProjectForm = form(Project.class).bindFromRequest();
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
PlayRepository repository = RepositoryService.getRepository(project);
if (validateWhenUpdate(ownerId, filledUpdatedProjectForm)) {
return badRequest(setting.render("title.projectSetting",
filledUpdatedProjectForm, project, repository.getBranches()));
}
Project updatedProject = filledUpdatedProjectForm.get();
FilePart filePart = request().body().asMultipartFormData().getFile("logoPath");
if (!isEmptyFilePart(filePart)) {
Attachment.deleteAll(updatedProject.asResource());
new Attachment().store(filePart.getFile(), filePart.getFilename(), updatedProject.asResource());
}
Map<String, String[]> data = request().body().asMultipartFormData().asFormUrlEncoded();
String defaultBranch = HttpUtil.getFirstValueFromQuery(data, "defaultBranch");
if (StringUtils.isNotEmpty(defaultBranch)) {
repository.setDefaultBranch(defaultBranch);
}
if (!project.name.equals(updatedProject.name)) {
if (!repository.renameTo(updatedProject.name)) {
throw new FileOperationException("fail repository rename to " + project.owner + "/" + updatedProject.name);
}
}
updatedProject.update();
saveProjectMenuSetting(updatedProject);
return redirect(routes.ProjectApp.settingForm(ownerId, updatedProject.name));
}
private static void saveProjectMenuSetting(Project project) {
Form<ProjectMenuSetting> filledUpdatedProjectMenuSettingForm = form(ProjectMenuSetting.class).bindFromRequest();
ProjectMenuSetting updatedProjectMenuSetting = filledUpdatedProjectMenuSettingForm.get();
project.refresh();
updatedProjectMenuSetting.project = project;
if (project.menuSetting == null) {
updatedProjectMenuSetting.save();
} else {
updatedProjectMenuSetting.id = project.menuSetting.id;
updatedProjectMenuSetting.update();
}
}
private static boolean validateWhenUpdate(String loginId, Form<Project> updateProjectForm) {
Long id = Long.parseLong(updateProjectForm.field("id").value());
String name = updateProjectForm.field("name").value();
if (!Project.projectNameChangeable(id, loginId, name)) {
flash(Constants.WARNING, "project.name.duplicate");
updateProjectForm.reject("name", "project.name.duplicate");
}
FilePart filePart = request().body().asMultipartFormData().getFile("logoPath");
if (!isEmptyFilePart(filePart)) {
if (!isImageFile(filePart.getFilename())) {
flash(Constants.WARNING, "project.logo.alert");
updateProjectForm.reject("logoPath", "project.logo.alert");
} else if (filePart.getFile().length() > LOGO_FILE_LIMIT_SIZE) {
flash(Constants.WARNING, "project.logo.fileSizeAlert");
updateProjectForm.reject("logoPath", "project.logo.fileSizeAlert");
}
}
ValidationError error = updateProjectForm.error("name");
if (error != null) {
if (PatternValidator.message.equals(error.message())) {
flash(Constants.WARNING, "project.name.alert");
updateProjectForm.errors().remove("name");
updateProjectForm.reject("name", "project.name.alert");
} else if (RestrictedValidator.message.equals(error.message())) {
flash(Constants.WARNING, "project.name.reserved.alert");
updateProjectForm.errors().remove("name");
updateProjectForm.reject("name", "project.name.reserved.alert");
}
}
return updateProjectForm.hasErrors();
}
@IsAllowed(Operation.DELETE)
public static Result deleteForm(String ownerId, String projectName) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
Form<Project> projectForm = form(Project.class).fill(project);
return ok(delete.render("title.projectDelete", projectForm, project));
}
@Transactional
@IsAllowed(Operation.DELETE)
public static Result deleteProject(String ownerId, String projectName) throws Exception {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
project.delete();
RepositoryService.deleteRepository(project);
if (HttpUtil.isRequestedWithXHR(request())){
response().setHeader("Location", routes.Application.index().toString());
return status(204);
}
return redirect(routes.Application.index());
}
@Transactional
@IsAllowed(Operation.UPDATE)
public static Result members(String loginId, String projectName) {
Project project = Project.findByOwnerAndProjectName(loginId, projectName);
project.cleanEnrolledUsers();
return ok(views.html.project.members.render("title.projectMembers",
ProjectUser.findMemberListByProject(project.id), project,
Role.findProjectRoles()));
}
@IsAllowed(Operation.READ)
public static Result mentionList(String loginId, String projectName, Long number, String resourceType) {
String prefer = HttpUtil.getPreferType(request(), HTML, JSON);
if (prefer == null) {
return status(Http.Status.NOT_ACCEPTABLE);
} else {
response().setHeader("Vary", "Accept");
}
Project project = Project.findByOwnerAndProjectName(loginId, projectName);
List<User> userList = new ArrayList<>();
collectAuthorAndCommenter(project, number, userList, resourceType);
addProjectMemberList(project, userList);
addGroupMemberList(project, userList);
userList.remove(UserApp.currentUser());
userList.add(UserApp.currentUser()); //send me last at list
Map<String, List<Map<String, String>>> result = new HashMap<>();
result.put("result", getUserList(project, userList));
result.put("issues", getIssueList(project));
return ok(toJson(result));
}
private static List<Map<String, String>> getIssueList(Project project) {
List<Map<String, String>> mentionListOfIssues = new ArrayList<>();
collectedIssuesToMap(mentionListOfIssues, getMentionIssueList(project));
return mentionListOfIssues;
}
private static List<Map<String, String>> getUserList(Project project, List<User> userList) {
List<Map<String, String>> mentionListOfUser = new ArrayList<>();
collectedUsersToMentionList(mentionListOfUser, userList);
addProjectNameToMentionList(mentionListOfUser, project);
addOrganizationNameToMentionList(mentionListOfUser, project);
return mentionListOfUser;
}
private static void addProjectNameToMentionList(List<Map<String, String>> users, Project project) {
Map<String, String> projectUserMap = new HashMap<>();
if(project != null){
projectUserMap.put("loginid", project.owner+"/" + project.name);
projectUserMap.put("username", project.name );
projectUserMap.put("name", project.name);
projectUserMap.put("image", urlToProjectLogo(project).toString());
users.add(projectUserMap);
}
}
private static void addOrganizationNameToMentionList(List<Map<String, String>> users, Project project) {
Map<String, String> projectUserMap = new HashMap<>();
if(project != null && project.organization != null){
projectUserMap.put("loginid", project.organization.name);
projectUserMap.put("username", project.organization.name);
projectUserMap.put("name", project.organization.name);
projectUserMap.put("image", urlToOrganizationLogo(project.organization).toString());
users.add(projectUserMap);
}
}
private static void collectedIssuesToMap(List<Map<String, String>> mentionList,
List<Issue> issueList) {
for (Issue issue : issueList) {
Map<String, String> projectIssueMap = new HashMap<>();
projectIssueMap.put("name", issue.getNumber().toString() + issue.title);
projectIssueMap.put("issueNo", issue.getNumber().toString());
projectIssueMap.put("title", issue.title);
mentionList.add(projectIssueMap);
}
}
private static List<Issue> getMentionIssueList(Project project) {
return Issue.finder.where()
.eq("project.id", project.isForkedFromOrigin() ? project.originalProject.id : project.id)
.orderBy("createdDate desc")
.setMaxRows(ISSUE_MENTION_SHOW_LIMIT)
.findList();
}
@IsAllowed(Operation.READ)
public static Result mentionListAtCommitDiff(String ownerId, String projectName, String commitId, Long pullRequestId)
throws IOException, UnsupportedOperationException, ServletException, SVNException {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
PullRequest pullRequest;
Project fromProject = project;
if (pullRequestId != -1) {
pullRequest = PullRequest.findById(pullRequestId);
if (pullRequest != null) {
fromProject = pullRequest.fromProject;
}
}
Commit commit = RepositoryService.getRepository(fromProject).getCommit(commitId);
List<User> userList = new ArrayList<>();
addCommitAuthor(commit, userList);
addCodeCommenters(commitId, fromProject.id, userList);
addProjectMemberList(project, userList);
addGroupMemberList(project, userList);
userList.remove(UserApp.currentUser());
userList.add(UserApp.currentUser()); //send me last at list
Map<String, List<Map<String, String>>> result = new HashMap<>();
result.put("result", getUserList(project, userList));
result.put("issues", getIssueList(project));
return ok(toJson(result));
}
@IsAllowed(Operation.READ)
public static Result mentionListAtPullRequest(String ownerId, String projectName, String commitId, Long pullRequestId)
throws IOException, UnsupportedOperationException, ServletException, SVNException {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
PullRequest pullRequest = PullRequest.findById(pullRequestId);
List<User> userList = new ArrayList<>();
addCommentAuthors(pullRequestId, userList);
addProjectMemberList(project, userList);
addGroupMemberList(project, userList);
if(!commitId.isEmpty()) {
addCommitAuthor(RepositoryService.getRepository(pullRequest.fromProject).getCommit(commitId), userList);
}
User contributor = pullRequest.contributor;
if(!userList.contains(contributor)) {
userList.add(contributor);
}
userList.remove(UserApp.currentUser());
userList.add(UserApp.currentUser()); //send me last at list
Map<String, List<Map<String, String>>> result = new HashMap<>();
result.put("result", getUserList(project, userList));
result.put("issues", getIssueList(project));
return ok(toJson(result));
}
private static void addCommentAuthors(Long pullRequestId, List<User> userList) {
List<CommentThread> threads = PullRequest.findById(pullRequestId).commentThreads;
for (CommentThread thread : threads) {
for (ReviewComment comment : thread.reviewComments) {
final User commenter = User.findByLoginId(comment.author.loginId);
if(userList.contains(commenter)) {
userList.remove(commenter);
}
userList.add(commenter);
}
}
Collections.reverse(userList);
}
@IsAllowed(Operation.DELETE)
public static Result transferForm(String ownerId, String projectName) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
Form<Project> projectForm = form(Project.class).fill(project);
return ok(transfer.render("title.projectTransfer", projectForm, project));
}
@Transactional
@IsAllowed(Operation.DELETE)
public static Result transferProject(String ownerId, String projectName) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
String destination = request().getQueryString("owner");
User destOwner = User.findByLoginId(destination);
Organization destOrg = Organization.findByName(destination);
if (destOwner.isAnonymous() && destOrg == null) {
return badRequest(ErrorViews.BadRequest.render());
}
User projectOwner = User.findByLoginId(project.owner);
Organization projectOrg = Organization.findByName(project.owner);
if((destOwner != null && destOwner.equals(projectOwner)) || (projectOrg != null && projectOrg.equals(destOrg))) {
flash(Constants.INFO, "project.transfer.has.same.owner");
Form<Project> projectForm = form(Project.class).fill(project);
return ok(transfer.render("title.projectTransfer", projectForm, project));
}
ProjectTransfer pt = null;
// make a request to move to an user
if (!destOwner.isAnonymous()) {
pt = ProjectTransfer.requestNewTransfer(project, UserApp.currentUser(), destOwner.loginId);
}
// make a request to move to an group
if (destOrg != null) {
pt = ProjectTransfer.requestNewTransfer(project, UserApp.currentUser(), destOrg.name);
}
sendTransferRequestMail(pt);
flash(Constants.INFO, "project.transfer.is.requested");
// if the request is sent by XHR, response with 204 204 No Content and Location header.
String url = routes.ProjectApp.project(ownerId, projectName).url();
if (HttpUtil.isRequestedWithXHR(request())) {
response().setHeader("Location", url);
return status(204);
}
return redirect(url);
}
@Transactional
@With(AnonymousCheckAction.class)
public static synchronized Result acceptTransfer(Long id, String confirmKey) throws IOException, ServletException {
ProjectTransfer pt = ProjectTransfer.findValidOne(id);
if (pt == null) {
return notFound(ErrorViews.NotFound.render());
}
if (confirmKey == null || !pt.confirmKey.equals(confirmKey)) {
return badRequest(ErrorViews.BadRequest.render());
}
if (!AccessControl.isAllowed(UserApp.currentUser(), pt.asResource(), Operation.ACCEPT)) {
return forbidden(ErrorViews.Forbidden.render());
}
Project project = pt.project;
// Change the project's name and move the repository.
String newProjectName = Project.newProjectName(pt.destination, project.name);
PlayRepository repository = RepositoryService.getRepository(project);
repository.move(project.owner, project.name, pt.destination, newProjectName);
User newOwnerUser = User.findByLoginId(pt.destination);
Organization newOwnerOrg = Organization.findByName(pt.destination);
// Change the project's information.
project.owner = pt.destination;
project.name = newProjectName;
if (newOwnerOrg != null) {
project.organization = newOwnerOrg;
} else {
project.organization = null;
}
project.update();
// Change roles.
if (!newOwnerUser.isAnonymous()) {
ProjectUser.assignRole(newOwnerUser.id, project.id, RoleType.MANAGER);
}
if (ProjectUser.isManager(pt.sender.id, project.id)) {
ProjectUser.assignRole(pt.sender.id, project.id, RoleType.MEMBER);
}
// Change the tranfer's status to be accepted.
pt.newProjectName = newProjectName;
pt.accepted = true;
pt.update();
// If the opposite request is exists, delete it.
ProjectTransfer.deleteExisting(project, pt.sender, pt.destination);
return redirect(routes.ProjectApp.project(project.owner, project.name));
}
@IsAllowed(Operation.UPDATE)
public static Result changeVCSForm(String ownerId, String projectName) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
Form<Project> projectForm = form(Project.class).fill(project);
return ok(change_vcs.render("title.projectChangeVCS", projectForm, project));
}
@IsAllowed(Operation.UPDATE)
public static Result changeVCS(String ownerId, String projectName) throws Exception {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
try {
if (project.readme() != null){
Posting posting = Posting.findREADMEPosting(project);
if (posting != null){
posting.readme = false;
posting.save();
}
}
project.changeVCS();
String url = routes.ProjectApp.project(ownerId, projectName).url();
response().setHeader("Location", url);
return noContent();
} catch (Exception e) {
Logger.error(e.getMessage());
}
return internalServerError();
}
private static void sendTransferRequestMail(ProjectTransfer pt) {
HtmlEmail email = new HtmlEmail();
try {
String acceptUrl = pt.getAcceptUrl();
String message = Messages.get("transfer.message.hello", pt.destination) + "\n\n"
+ Messages.get("transfer.message.detail", pt.project.name, pt.newProjectName, pt.project.owner, pt.destination) + "\n"
+ Messages.get("transfer.message.link") + "\n\n"
+ acceptUrl + "\n\n"
+ Messages.get("transfer.message.deadline") + "\n\n"
+ Messages.get("transfer.message.thank");
email.setFrom(Config.getEmailFromSmtp(), pt.sender.name);
email.addTo(Config.getEmailFromSmtp(), "Yobi");
User to = User.findByLoginId(pt.destination);
if (!to.isAnonymous()) {
email.addBcc(to.email, to.name);
}
Organization org = Organization.findByName(pt.destination);
if (org != null) {
List<OrganizationUser> admins = OrganizationUser.findAdminsOf(org);
for(OrganizationUser admin : admins) {
email.addBcc(admin.user.email, admin.user.name);
}
}
email.setSubject(String.format("[%s] @%s wants to transfer project", pt.project.name, pt.sender.loginId));
email.setHtmlMsg(Markdown.render(message));
email.setTextMsg(message);
email.setCharset("utf-8");
email.addHeader("References", "<" + acceptUrl + "@" + Config.getHostname() + ">");
email.setSentDate(pt.requested);
Mailer.send(email);
String escapedTitle = email.getSubject().replace("\"", "\\\"");
String logEntry = String.format("\"%s\" %s", escapedTitle, email.getBccAddresses());
play.Logger.of("mail").info(logEntry);
} catch (Exception e) {
Logger.warn("Failed to send a notification: " + email + "\n" + ExceptionUtils.getStackTrace(e));
}
}
private static void addCodeCommenters(String commitId, Long projectId, List<User> userList) {
Project project = Project.find.byId(projectId);
if (RepositoryService.VCS_GIT.equals(project.vcs)) {
List<ReviewComment> comments = ReviewComment.find
.fetch("thread")
.where()
.eq("thread.commitId",commitId)
.eq("thread.project", project)
.eq("thread.pullRequest", null).findList();
for (ReviewComment comment : comments) {
User commentAuthor = User.findByLoginId(comment.author.loginId);
if (userList.contains(commentAuthor)) {
userList.remove(commentAuthor);
}
userList.add(commentAuthor);
}
} else {
List<CommitComment> comments = CommitComment.find.where().eq("commitId",
commitId).eq("project.id", projectId).findList();
for (CommitComment codeComment : comments) {
User commentAuthor = User.findByLoginId(codeComment.authorLoginId);
if (userList.contains(commentAuthor)) {
userList.remove(commentAuthor);
}
userList.add(commentAuthor);
}
}
Collections.reverse(userList);
}
private static void addCommitAuthor(Commit commit, List<User> userList) {
if (!commit.getAuthor().isAnonymous() && !userList.contains(commit.getAuthor())) {
userList.add(commit.getAuthor());
}
//fallback: additional search by email id
if (commit.getAuthorEmail() != null) {
User authorByEmail = User.findByLoginId(commit.getAuthorEmail().substring(0, commit.getAuthorEmail().lastIndexOf("@")));
if (!authorByEmail.isAnonymous() && !userList.contains(authorByEmail)) {
userList.add(authorByEmail);
}
}
}
private static void collectAuthorAndCommenter(Project project, Long number, List<User> userList, String resourceType) {
AbstractPosting posting;
switch (ResourceType.getValue(resourceType)) {
case ISSUE_POST:
posting = AbstractPosting.findByNumber(Issue.finder, project, number);
break;
case BOARD_POST:
posting = AbstractPosting.findByNumber(Posting.finder, project, number);
break;
default:
return;
}
if (posting != null) {
for (Comment comment: posting.getComments()) {
User commentUser = User.findByLoginId(comment.authorLoginId);
if (userList.contains(commentUser)) {
userList.remove(commentUser);
}
userList.add(commentUser);
}
Collections.reverse(userList); // recent commenter first!
User postAuthor = User.findByLoginId(posting.authorLoginId);
if (!userList.contains(postAuthor)) {
userList.add(postAuthor);
}
}
}
private static void collectedUsersToMentionList(List<Map<String, String>> users, List<User> userList) {
for (User user: userList) {
Map<String, String> projectUserMap = new HashMap<>();
if (user != null && !user.loginId.equals(Constants.ADMIN_LOGIN_ID)) {
projectUserMap.put("loginid", user.loginId);
projectUserMap.put("username", user.name);
projectUserMap.put("name", user.name + user.loginId);
projectUserMap.put("image", user.avatarUrl());
users.add(projectUserMap);
}
}
}
private static void addProjectMemberList(Project project, List<User> userList) {
for (ProjectUser projectUser: project.projectUser) {
if (!userList.contains(projectUser.user)) {
userList.add(projectUser.user);
}
}
}
private static void addGroupMemberList(Project project, List<User> userList) {
if (!project.hasGroup()) {
return;
}
for (OrganizationUser organizationUser : project.organization.users) {
if (!userList.contains(organizationUser.user)) {
userList.add(organizationUser.user);
}
}
}
@Transactional
@With(DefaultProjectCheckAction.class)
@IsAllowed(Operation.UPDATE)
public static Result newMember(String ownerId, String projectName) {
Form<User> addMemberForm = form(User.class).bindFromRequest();
User newMember = User.findByLoginId(addMemberForm.field("loginId").value());
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
if (isErrorOnAddMemberForm(newMember, project, addMemberForm)) {
if(HttpUtil.isJSONPreferred(request())){
return badRequest(addMemberForm.errorsAsJson());
}
List<ValidationError> errors = addMemberForm.errors().get("loginId");
flash(Constants.WARNING, errors.get(errors.size() - 1).message());
return redirect(routes.ProjectApp.members(ownerId, projectName));
}
ProjectUser.assignRole(newMember.id, project.id, RoleType.MEMBER);
project.cleanEnrolledUsers();
NotificationEvent.afterMemberRequest(project, newMember, RequestState.ACCEPT);
if(HttpUtil.isJSONPreferred(request())){
return ok("{}");
}
return redirect(routes.ProjectApp.members(ownerId, projectName));
}
private static boolean isErrorOnAddMemberForm(User user, Project project, Form<User> addMemberForm) {
if (addMemberForm.hasErrors()) {
addMemberForm.reject("loginId", "project.members.addMember");
} else if (!AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
addMemberForm.reject("loginId", "project.member.isManager");
} else if (user.isAnonymous()) {
addMemberForm.reject("loginId", "project.member.notExist");
} else if (ProjectUser.isMember(user.id, project.id)) {
addMemberForm.reject("loginId", "project.member.alreadyMember");
}
return addMemberForm.hasErrors();
}
/**
* Returns OK(200) with {@code location} which is represented as JSON .
*
* Since returning redirect response(3xx) to Ajax request causes unexpected result, this function returns OK(200) containing redirect location.
* The client will check {@code location} and have a page move to the location.
*
* @param location
* @return
*/
private static Result okWithLocation(String location) {
ObjectNode result = Json.newObject();
result.put("location", location);
return ok(result);
}
/**
* @param ownerId the user login id
* @param projectName the project name
* @param userId
* @return the result
*/
@Transactional
@With(DefaultProjectCheckAction.class)
public static Result deleteMember(String ownerId, String projectName, Long userId) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
User deleteMember = User.find.byId(userId);
if (!UserApp.currentUser().id.equals(userId)
&& !AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.UPDATE)) {
return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
}
if (project.isOwner(deleteMember)) {
return forbidden(ErrorViews.Forbidden.render("project.member.ownerCannotLeave", project));
}
ProjectUser.delete(userId, project.id);
if (UserApp.currentUser().id.equals(userId)) {
if (AccessControl.isAllowed(UserApp.currentUser(), project.asResource(), Operation.READ)) {
return okWithLocation(routes.ProjectApp.project(project.owner, project.name).url());
} else {
return okWithLocation(routes.Application.index().url());
}
} else {
return okWithLocation(routes.ProjectApp.members(ownerId, projectName).url());
}
}
/**
* @param ownerId the user login id
* @param projectName the project name
* @param userId the user id
* @return
*/
@Transactional
@IsAllowed(Operation.UPDATE)
public static Result editMember(String ownerId, String projectName, Long userId) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
User editMember = User.find.byId(userId);
if (project.isOwner(editMember)) {
return badRequest(ErrorViews.Forbidden.render("project.member.ownerMustBeAManager", project));
}
ProjectUser.assignRole(userId, project.id, form(Role.class).bindFromRequest().get().id);
return status(Http.Status.NO_CONTENT);
}
/**
* @param query the query
* @param pageNum the page num
* @return
*/
public static Result projects(String query, int pageNum) {
String prefer = HttpUtil.getPreferType(request(), HTML, JSON);
if (prefer == null) {
return status(Http.Status.NOT_ACCEPTABLE);
}
response().setHeader("Vary", "Accept");
if (prefer.equals(JSON)) {
return getProjectsToJSON(query);
} else {
return getPagingProjects(query, pageNum);
}
}
private static Result getPagingProjects(String query, int pageNum) {
ExpressionList<Project> el = createProjectSearchExpressionList(query);
Set<Long> labelIds = LabelSearchUtil.getLabelIds(request());
if (CollectionUtils.isNotEmpty(labelIds)) {
el.add(LabelSearchUtil.createLabelSearchExpression(el.query(), labelIds));
}
el.orderBy("createdDate desc");
Page<Project> projects = el.findPagingList(PROJECT_COUNT_PER_PAGE).getPage(pageNum - 1);
return ok(views.html.project.list.render("title.projectList", projects, query));
}
private static Result getProjectsToJSON(String query) {
ExpressionList<Project> el = createProjectSearchExpressionList(query);
int total = el.findRowCount();
if (total > MAX_FETCH_PROJECTS) {
el.setMaxRows(MAX_FETCH_PROJECTS);
response().setHeader("Content-Range", "items " + MAX_FETCH_PROJECTS + "/" + total);
}
List<String> projectNames = new ArrayList<>();
for (Project project: el.findList()) {
projectNames.add(project.owner + "/" + project.name);
}
return ok(toJson(projectNames));
}
private static ExpressionList<Project> createProjectSearchExpressionList(String query) {
ExpressionList<Project> el = Project.find.where();
if (StringUtils.isNotBlank(query)) {
Junction<Project> junction = el.disjunction();
junction.icontains("owner", query)
.icontains("name", query)
.icontains("overview", query);
List<Object> ids = Project.find.where().icontains("labels.name", query).findIds();
if (!ids.isEmpty()) {
junction.idIn(ids);
}
junction.endJunction();
}
if (!UserApp.currentUser().isSiteManager()) {
el.eq("projectScope", ProjectScope.PUBLIC);
}
return el;
}
/**
* @param ownerId the owner login id
* @param projectName the project name
* @return
*/
@IsAllowed(Operation.READ)
public static Result labels(String ownerId, String projectName) {
if (!request().accepts("application/json")) {
return status(Http.Status.NOT_ACCEPTABLE);
}
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
Map<Long, Map<String, String>> labels = new HashMap<>();
for (Label label: project.labels) {
labels.put(label.id, convertToMap(label));
}
return ok(toJson(labels));
}
/**
* convert from some part of {@link models.Label} to {@link java.util.Map}
* @param label {@link models.Label} object
* @return label's map data
*/
private static Map<String, String> convertToMap(Label label) {
Map<String, String> tagMap = new HashMap<>();
tagMap.put("category", label.category);
tagMap.put("name", label.name);
return tagMap;
}
/**
* @param ownerId the owner name
* @param projectName the project name
* @return the result
*/
@Transactional
@With(DefaultProjectCheckAction.class)
public static Result attachLabel(String ownerId, String projectName) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
if (!AccessControl.isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)) {
return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
}
// Get category and name from the request. Return 400 Bad Request if name is not given.
Map<String, String[]> data = request().body().asFormUrlEncoded();
String category = HttpUtil.getFirstValueFromQuery(data, "category");
String name = HttpUtil.getFirstValueFromQuery(data, "name");
if (StringUtils.isEmpty(name)) {
// A label must have its name.
return badRequest(ErrorViews.BadRequest.render("Label name is missing.", project));
}
Label label = Label.find
.where().eq("category", category).eq("name", name).findUnique();
boolean isCreated = false;
if (label == null) {
// Create new label if there is no label which has the given name.
label = new Label(category, name);
label.projects.add(project);
label.save();
isCreated = true;
}
Boolean isAttached = project.attachLabel(label);
if (!isCreated && !isAttached) {
// Something is wrong. This case is not possible.
play.Logger.warn(
"A label '" + label + "' is created but failed to attach to project '" + project + "'.");
}
if (isAttached) {
// Return the attached label. The return type is Map<Long, Map<String, String>>
// even if there is only one label, to unify the return type with
// ProjectApp.labels().
Map<Long, Map<String, String>> labels = new HashMap<>();
labels.put(label.id, convertToMap(label));
if (isCreated) {
return created(toJson(labels));
} else {
return ok(toJson(labels));
}
} else {
// Return 204 No Content if the label is already attached.
return status(Http.Status.NO_CONTENT);
}
}
/**
* @param ownerId the owner name
* @param projectName the project name
* @param id the id
* @return the result
*/
@Transactional
@With(DefaultProjectCheckAction.class)
public static Result detachLabel(String ownerId, String projectName, Long id) {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
if (!AccessControl.isAllowed(UserApp.currentUser(), project.labelsAsResource(), Operation.UPDATE)) {
return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
}
// _method must be 'delete'
Map<String, String[]> data = request().body().asFormUrlEncoded();
if (!HttpUtil.getFirstValueFromQuery(data, "_method").toLowerCase()
.equals("delete")) {
return badRequest(ErrorViews.BadRequest.render("_method must be 'delete'.", project));
}
Label tag = Label.find.byId(id);
if (tag == null) {
return notFound(ErrorViews.NotFound.render("error.notfound"));
}
project.detachLabel(tag);
return status(Http.Status.NO_CONTENT);
}
@Transactional
@With(AnonymousCheckAction.class)
@IsAllowed(Operation.DELETE)
public static Result deletePushedBranch(String ownerId, String projectName, Long id) {
PushedBranch pushedBranch = PushedBranch.find.byId(id);
if (pushedBranch != null) {
pushedBranch.delete();
}
return ok();
}
}