/**
* Yona, 21st Century Project Hosting SW
*
* Copyright Yona & Yobi Authors & NAVER Corp. & NAVER LABS Corp.
* https://yona.io
**/
package controllers;
import actions.DefaultProjectCheckAction;
import com.avaje.ebean.*;
import controllers.annotation.AnonymousCheck;
import controllers.annotation.GuestProhibit;
import controllers.annotation.IsAllowed;
import info.schleichardt.play2.mailplugin.Mailer;
import jxl.write.WriteException;
import models.*;
import models.enumeration.*;
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 com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.tmatesoft.svn.core.SVNException;
import models.resource.Resource;
import play.Logger;
import play.data.Form;
import play.data.validation.Constraints.PatternValidator;
import play.data.validation.ValidationError;
import play.db.ebean.Transactional;
import play.i18n.Messages;
import play.libs.Json;
import play.mvc.*;
import play.mvc.Http.MultipartFormData.FilePart;
import playRepository.Commit;
import playRepository.PlayRepository;
import playRepository.RepositoryService;
import scala.reflect.io.FileOperationException;
import utils.*;
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 com.avaje.ebean.Expr.ilike;
import static play.data.Form.form;
import static play.libs.Json.toJson;
import static utils.CacheStore.getProjectCacheKey;
import static utils.CacheStore.projectMap;
import static utils.LogoUtil.*;
import static utils.TemplateHelper.*;
@AnonymousCheck
public class ProjectApp extends Controller {
private static final int ISSUE_MENTION_SHOW_LIMIT = 20;
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";
@AnonymousCheck(requiresLogin = true, displaysFlashMessage = true)
@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").textValue();
targetProject.save();
ObjectNode result = Json.newObject();
result.put("overview", targetProject.overview);
return ok(result);
}
@IsAllowed(Operation.READ)
@Transactional
public static Result project(String ownerId, String projectName)
throws IOException, ServletException, SVNException, GitAPIException {
Project project = Project.findByOwnerAndProjectName(ownerId, projectName);
List histories = null;
String tabId = StringUtils.defaultIfBlank(request().getQueryString("tabId"), "readme");
if(!tabId.equals("readme")){
histories = getProjectHistory(ownerId, project);
}
UserApp.currentUser().visits(project);
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 getProjectHistory(String ownerId, Project project)
throws IOException, ServletException, SVNException, GitAPIException {
project.fixInvalidForkData();
PlayRepository repository = RepositoryService.getRepository(project);
List commits = null;
try {
commits = repository.getHistory(COMMIT_HISTORY_PAGE, COMMIT_HISTORY_SHOW_LIMIT, null, null);
} catch (NoHeadException e) {
// NOOP
}
List issues = Issue.findRecentlyCreated(project, RECENLTY_ISSUE_SHOW_LIMIT);
List postings = Posting.findRecentlyCreated(project, RECENLTY_POSTING_SHOW_LIMIT);
List pullRequests = PullRequest.findRecentlyReceived(project, RECENT_PULL_REQUEST_SHOW_LIMIT);
return History.makeHistory(ownerId, project, commits, issues, postings, pullRequests);
}
@AnonymousCheck(requiresLogin = true, displaysFlashMessage = true)
public static Result newProjectForm() {
Form projectForm = form(Project.class).bindFromRequest("owner");
projectForm.discardErrors();
List 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 projectForm = form(Project.class).fill(project);
PlayRepository repository = RepositoryService.getRepository(project);
return ok(setting.render("title.projectSetting", projectForm, project, repository.getRefNames()));
}
@Transactional
public static Result newProject() throws Exception {
Form 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.getDisplayName() + "' 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);
Watch.watch(project.asResource());
projectMap.put(getProjectCacheKey(project.owner, project.name), project.id);
UserApp.currentUser().visits(project);
return redirect(routes.ProjectApp.project(project.owner, project.name));
}
private static boolean validateWhenNew(Form 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 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.getRefNames()));
}
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 data = request().body().asMultipartFormData().asFormUrlEncoded();
String defaultBranch = HttpUtil.getFirstValueFromQuery(data, "defaultBranch");
if (StringUtils.isNotEmpty(defaultBranch)) {
repository.setDefaultBranch(defaultBranch);
}
if (!project.name.equals(updatedProject.name)) {
updatedProject.recordRenameOrTransferHistoryIfLastChangePassed24HoursFrom(project);
if (!repository.renameTo(updatedProject.name)) {
throw new FileOperationException("fail repository rename to " + project.owner + "/" + updatedProject.name);
}
CacheStore.refreshProjectMap();
}
updatedProject.update();
saveProjectMenuSetting(updatedProject);
UserApp.currentUser().updateFavoriteProject(updatedProject);
FavoriteProject.updateFavoriteProject(updatedProject);
return redirect(routes.ProjectApp.settingForm(ownerId, updatedProject.name));
}
public static void saveProjectMenuSetting(Project project) {
Form filledUpdatedProjectMenuSettingForm = form(ProjectMenuSetting.class).bindFromRequest();
ProjectMenuSetting updatedProjectMenuSetting = filledUpdatedProjectMenuSettingForm.get();
project.refresh();
updatedProjectMenuSetting.project = project;
if (project.menuSetting == null) {
updatedProjectMenuSetting.save();
} else {
project.menuSetting.updateMenuSetting(updatedProjectMenuSetting);
}
}
private static boolean validateWhenUpdate(String loginId, Form 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 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);
if (project == null) {
return redirect(routes.Application.index());
}
UserApp.currentUser().removeFavoriteProject(project.id);
project.delete();
RepositoryService.deleteRepository(project);
CacheStore.refreshProjectMap();
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()));
}
@Transactional
@IsAllowed(Operation.READ)
public static Result watchers(String loginId, String projectName) {
Project project = Project.findByOwnerAndProjectName(loginId, projectName);
Resource resource = project.asResource();
return ok(views.html.project.watchers.render(
"title.projectWatchers",
Watch.findActualWatchers(
Watch.findWatchers(resource.getType(), resource.getId()),
resource,
true
),
project
));
}
@AnonymousCheck
public static Result mentionList(String loginId, String projectName, Long number,
String resourceType, String query, String mentionType) {
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 userList = new ArrayList<>();
Map>> result = new HashMap<>();
if("user".equalsIgnoreCase(mentionType)){
if (StringUtils.isEmpty(query) || !project.isPublic()) {
collectAuthorAndCommenter(project, number, userList, resourceType);
addProjectMemberList(project, userList);
addGroupMemberList(project, userList);
addProjectAuthorsAndWatchersList(project, userList);
addSharers(project, number, userList, resourceType);
} else {
addSearchedUsers(query, userList);
}
userList.remove(UserApp.currentUser());
userList.add(UserApp.currentUser()); //send me last at list
result.put("result", getUserList(project, userList));
}
if("issue".equalsIgnoreCase(mentionType)) {
result.put("result", getIssueList(project, query));
}
return ok(toJson(result));
}
private static List