forgejo/routers/web/repo/projects.go
zeripath c9c0475f4d
In Render tolerate not being passed a context (#16842)
* In Render tolerate not being passed a context

It is possible for RenderString to be passed to an external renderer if markdown
is set to be rendered by an external renderer. No context is currently sent to these
meaning that this will error out.

Fix #16835

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add Context to Repo calls for RenderString

All calls from routers can easily add the context - so add it.

Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-08-28 21:15:56 +01:00

669 lines
17 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
)
const (
tplProjects base.TplName = "repo/projects/list"
tplProjectsNew base.TplName = "repo/projects/new"
tplProjectsView base.TplName = "repo/projects/view"
tplGenericProjectsNew base.TplName = "user/project"
)
// MustEnableProjects check if projects are enabled in settings
func MustEnableProjects(ctx *context.Context) {
if models.UnitTypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil)
return
}
if ctx.Repo.Repository != nil {
if !ctx.Repo.CanRead(models.UnitTypeProjects) {
ctx.NotFound("MustEnableProjects", nil)
return
}
}
}
// Projects renders the home page of projects
func Projects(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.project_board")
sortType := ctx.FormTrim("sort")
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
repo := ctx.Repo.Repository
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
ctx.Data["OpenCount"] = repo.NumOpenProjects
ctx.Data["ClosedCount"] = repo.NumClosedProjects
var total int
if !isShowClosed {
total = repo.NumOpenProjects
} else {
total = repo.NumClosedProjects
}
projects, count, err := models.GetProjects(models.ProjectSearchOptions{
RepoID: repo.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
SortType: sortType,
Type: models.ProjectTypeRepository,
})
if err != nil {
ctx.ServerError("GetProjects", err)
return
}
for i := range projects {
projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, projects[i].Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
}
ctx.Data["Projects"] = projects
if isShowClosed {
ctx.Data["State"] = "closed"
} else {
ctx.Data["State"] = "open"
}
numPages := 0
if count > 0 {
numPages = int((int(count) - 1) / setting.UI.IssuePagingNum)
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
pager.AddParam(ctx, "state", "State")
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["IsProjectsPage"] = true
ctx.Data["SortType"] = sortType
ctx.HTML(http.StatusOK, tplProjects)
}
// NewProject render creating a project page
func NewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
// NewProjectPost creates a new project
func NewProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
if ctx.HasError() {
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
ctx.HTML(http.StatusOK, tplProjectsNew)
return
}
if err := models.NewProject(&models.Project{
RepoID: ctx.Repo.Repository.ID,
Title: form.Title,
Description: form.Content,
CreatorID: ctx.User.ID,
BoardType: form.BoardType,
Type: models.ProjectTypeRepository,
}); err != nil {
ctx.ServerError("NewProject", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
}
// ChangeProjectStatus updates the status of a project between "open" and "close"
func ChangeProjectStatus(ctx *context.Context) {
toClose := false
switch ctx.Params(":action") {
case "open":
toClose = false
case "close":
toClose = true
default:
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
}
id := ctx.ParamsInt64(":id")
if err := models.ChangeProjectStatusByRepoIDAndID(ctx.Repo.Repository.ID, id, toClose); err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", err)
} else {
ctx.ServerError("ChangeProjectStatusByIDAndRepoID", err)
}
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/projects?state=" + ctx.Params(":action"))
}
// DeleteProject delete a project
func DeleteProject(ctx *context.Context) {
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
if err := models.DeleteProjectByID(p.ID); err != nil {
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/projects",
})
}
// EditProject allows a project to be edited
func EditProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsProjects"] = true
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
ctx.Data["title"] = p.Title
ctx.Data["content"] = p.Description
ctx.HTML(http.StatusOK, tplProjectsNew)
}
// EditProjectPost response for editing a project
func EditProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsProjects"] = true
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplProjectsNew)
return
}
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
p.Title = form.Title
p.Description = form.Content
if err = models.UpdateProject(p); err != nil {
ctx.ServerError("UpdateProjects", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
ctx.Redirect(ctx.Repo.RepoLink + "/projects")
}
// ViewProject renders the project board for a project
func ViewProject(ctx *context.Context) {
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
boards, err := models.GetProjectBoards(project.ID)
if err != nil {
ctx.ServerError("GetProjectBoards", err)
return
}
if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
}
issueList, err := boards.LoadIssues()
if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err)
return
}
ctx.Data["Issues"] = issueList
linkedPrsMap := make(map[int64][]*models.Issue)
for _, issue := range issueList {
var referencedIds []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
referencedIds = append(referencedIds, comment.RefIssueID)
}
}
if len(referencedIds) > 0 {
if linkedPrs, err := models.Issues(&models.IssuesOptions{
IssueIDs: referencedIds,
IsPull: util.OptionalBoolTrue,
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
}
}
ctx.Data["LinkedPRs"] = linkedPrsMap
project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, project.Description)
if err != nil {
ctx.ServerError("RenderString", err)
return
}
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.Data["Project"] = project
ctx.Data["Boards"] = boards
ctx.Data["PageIsProjects"] = true
ctx.Data["RequiresDraggable"] = true
ctx.HTML(http.StatusOK, tplProjectsView)
}
// UpdateIssueProject change an issue's project
func UpdateIssueProject(ctx *context.Context) {
issues := getActionIssues(ctx)
if ctx.Written() {
return
}
projectID := ctx.FormInt64("id")
for _, issue := range issues {
oldProjectID := issue.ProjectID()
if oldProjectID == projectID {
continue
}
if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// DeleteProjectBoard allows for the deletion of a project board
func DeleteProjectBoard(ctx *context.Context) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
pb, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return
}
if pb.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
})
return
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
})
return
}
if err := models.DeleteProjectBoardByID(ctx.ParamsInt64(":boardID")); err != nil {
ctx.ServerError("DeleteProjectBoardByID", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// AddBoardToProjectPost allows a new board to be added to a project.
func AddBoardToProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if err := models.NewProjectBoard(&models.ProjectBoard{
ProjectID: project.ID,
Title: form.Title,
CreatorID: ctx.User.ID,
}); err != nil {
ctx.ServerError("NewProjectBoard", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
func checkProjectBoardChangePermissions(ctx *context.Context) (*models.Project, *models.ProjectBoard) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return nil, nil
}
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return nil, nil
}
project, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return nil, nil
}
board, err := models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return nil, nil
}
if board.ProjectID != ctx.ParamsInt64(":id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
})
return nil, nil
}
if project.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
})
return nil, nil
}
return project, board
}
// EditProjectBoard allows a project board's to be updated
func EditProjectBoard(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
_, board := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}
if form.Title != "" {
board.Title = form.Title
}
if form.Sorting != 0 {
board.Sorting = form.Sorting
}
if err := models.UpdateProjectBoard(board); err != nil {
ctx.ServerError("UpdateProjectBoard", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
func SetDefaultProjectBoard(ctx *context.Context) {
project, board := checkProjectBoardChangePermissions(ctx)
if ctx.Written() {
return
}
if err := models.SetDefaultBoard(project.ID, board.ID); err != nil {
ctx.ServerError("SetDefaultBoard", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// MoveIssueAcrossBoards move a card from one board to another in a project
func MoveIssueAcrossBoards(ctx *context.Context) {
if ctx.User == nil {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in users are allowed to perform this action.",
})
return
}
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(models.AccessModeWrite, models.UnitTypeProjects) {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only authorized users are allowed to perform this action.",
})
return
}
p, err := models.GetProjectByID(ctx.ParamsInt64(":id"))
if err != nil {
if models.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound("", nil)
return
}
var board *models.ProjectBoard
if ctx.ParamsInt64(":boardID") == 0 {
board = &models.ProjectBoard{
ID: 0,
ProjectID: 0,
Title: ctx.Tr("repo.projects.type.uncategorized"),
}
} else {
board, err = models.GetProjectBoard(ctx.ParamsInt64(":boardID"))
if err != nil {
if models.IsErrProjectBoardNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetProjectBoard", err)
}
return
}
if board.ProjectID != p.ID {
ctx.NotFound("", nil)
return
}
}
issue, err := models.GetIssueByID(ctx.ParamsInt64(":index"))
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound("", nil)
} else {
ctx.ServerError("GetIssueByID", err)
}
return
}
if err := models.MoveIssueAcrossProjectBoards(issue, board); err != nil {
ctx.ServerError("MoveIssueAcrossProjectBoards", err)
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
})
}
// CreateProject renders the generic project creation page
func CreateProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["ProjectTypes"] = models.GetProjectsConfig()
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.HTML(http.StatusOK, tplGenericProjectsNew)
}
// CreateProjectPost creates an individual and/or organization project
func CreateProjectPost(ctx *context.Context, form forms.UserCreateProjectForm) {
user := checkContextUser(ctx, form.UID)
if ctx.Written() {
return
}
ctx.Data["ContextUser"] = user
if ctx.HasError() {
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects)
ctx.HTML(http.StatusOK, tplGenericProjectsNew)
return
}
var projectType = models.ProjectTypeIndividual
if user.IsOrganization() {
projectType = models.ProjectTypeOrganization
}
if err := models.NewProject(&models.Project{
Title: form.Title,
Description: form.Content,
CreatorID: user.ID,
BoardType: form.BoardType,
Type: projectType,
}); err != nil {
ctx.ServerError("NewProject", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/")
}