mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Recent modernize fixes: https://github.com/golang/tools/commits/master/gopls/internal/analysis/modernize
		
			
				
	
	
		
			693 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			693 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2014 The Gogs Authors. All rights reserved.
 | 
						|
// Copyright 2018 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package repo
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"html/template"
 | 
						|
	"net/http"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/models/db"
 | 
						|
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						|
	"code.gitea.io/gitea/models/organization"
 | 
						|
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
						|
	project_model "code.gitea.io/gitea/models/project"
 | 
						|
	"code.gitea.io/gitea/models/renderhelper"
 | 
						|
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						|
	"code.gitea.io/gitea/models/unit"
 | 
						|
	user_model "code.gitea.io/gitea/models/user"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
						|
	"code.gitea.io/gitea/modules/optional"
 | 
						|
	api "code.gitea.io/gitea/modules/structs"
 | 
						|
	"code.gitea.io/gitea/modules/templates"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	"code.gitea.io/gitea/modules/web"
 | 
						|
	"code.gitea.io/gitea/routers/common"
 | 
						|
	"code.gitea.io/gitea/services/context"
 | 
						|
	"code.gitea.io/gitea/services/convert"
 | 
						|
	"code.gitea.io/gitea/services/forms"
 | 
						|
	issue_service "code.gitea.io/gitea/services/issue"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	tplAttachment templates.TplName = "repo/issue/view_content/attachments"
 | 
						|
 | 
						|
	tplIssues      templates.TplName = "repo/issue/list"
 | 
						|
	tplIssueNew    templates.TplName = "repo/issue/new"
 | 
						|
	tplIssueChoose templates.TplName = "repo/issue/choose"
 | 
						|
	tplIssueView   templates.TplName = "repo/issue/view"
 | 
						|
 | 
						|
	tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
 | 
						|
	tplReactions    templates.TplName = "repo/issue/view_content/reactions"
 | 
						|
 | 
						|
	issueTemplateKey      = "IssueTemplate"
 | 
						|
	issueTemplateTitleKey = "IssueTemplateTitle"
 | 
						|
)
 | 
						|
 | 
						|
// IssueTemplateCandidates issue templates
 | 
						|
var IssueTemplateCandidates = []string{
 | 
						|
	"ISSUE_TEMPLATE.md",
 | 
						|
	"ISSUE_TEMPLATE.yaml",
 | 
						|
	"ISSUE_TEMPLATE.yml",
 | 
						|
	"issue_template.md",
 | 
						|
	"issue_template.yaml",
 | 
						|
	"issue_template.yml",
 | 
						|
	".gitea/ISSUE_TEMPLATE.md",
 | 
						|
	".gitea/ISSUE_TEMPLATE.yaml",
 | 
						|
	".gitea/ISSUE_TEMPLATE.yml",
 | 
						|
	".gitea/issue_template.md",
 | 
						|
	".gitea/issue_template.yaml",
 | 
						|
	".gitea/issue_template.yml",
 | 
						|
	".github/ISSUE_TEMPLATE.md",
 | 
						|
	".github/ISSUE_TEMPLATE.yaml",
 | 
						|
	".github/ISSUE_TEMPLATE.yml",
 | 
						|
	".github/issue_template.md",
 | 
						|
	".github/issue_template.yaml",
 | 
						|
	".github/issue_template.yml",
 | 
						|
}
 | 
						|
 | 
						|
// MustAllowUserComment checks to make sure if an issue is locked.
 | 
						|
// If locked and user has permissions to write to the repository,
 | 
						|
// then the comment is allowed, else it is blocked
 | 
						|
func MustAllowUserComment(ctx *context.Context) {
 | 
						|
	issue := GetActionIssue(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
 | 
						|
		ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
 | 
						|
		ctx.Redirect(issue.Link())
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// MustEnableIssues check if repository enable internal issues
 | 
						|
func MustEnableIssues(ctx *context.Context) {
 | 
						|
	if !ctx.Repo.CanRead(unit.TypeIssues) &&
 | 
						|
		!ctx.Repo.CanRead(unit.TypeExternalTracker) {
 | 
						|
		ctx.NotFound(nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
 | 
						|
	if err == nil {
 | 
						|
		ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// MustAllowPulls check if repository enable pull requests and user have right to do that
 | 
						|
func MustAllowPulls(ctx *context.Context) {
 | 
						|
	if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
 | 
						|
		ctx.NotFound(nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// User can send pull request if owns a forked repository.
 | 
						|
	if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
 | 
						|
		ctx.Repo.PullRequest.Allowed = true
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) {
 | 
						|
	// Distinguish whether the owner of the repository
 | 
						|
	// is an individual or an organization
 | 
						|
	repoOwnerType := project_model.TypeIndividual
 | 
						|
	if repo.Owner.IsOrganization() {
 | 
						|
		repoOwnerType = project_model.TypeOrganization
 | 
						|
	}
 | 
						|
 | 
						|
	projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
 | 
						|
 | 
						|
	var openProjects []*project_model.Project
 | 
						|
	var closedProjects []*project_model.Project
 | 
						|
	var err error
 | 
						|
 | 
						|
	if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
 | 
						|
		openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
 | 
						|
			ListOptions: db.ListOptionsAll,
 | 
						|
			RepoID:      repo.ID,
 | 
						|
			IsClosed:    optional.Some(false),
 | 
						|
			Type:        project_model.TypeRepository,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetProjects", err)
 | 
						|
			return nil, nil
 | 
						|
		}
 | 
						|
		closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
 | 
						|
			ListOptions: db.ListOptionsAll,
 | 
						|
			RepoID:      repo.ID,
 | 
						|
			IsClosed:    optional.Some(true),
 | 
						|
			Type:        project_model.TypeRepository,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetProjects", err)
 | 
						|
			return nil, nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
 | 
						|
		openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
 | 
						|
			ListOptions: db.ListOptionsAll,
 | 
						|
			OwnerID:     repo.OwnerID,
 | 
						|
			IsClosed:    optional.Some(false),
 | 
						|
			Type:        repoOwnerType,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetProjects", err)
 | 
						|
			return nil, nil
 | 
						|
		}
 | 
						|
		openProjects = append(openProjects, openProjects2...)
 | 
						|
		closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
 | 
						|
			ListOptions: db.ListOptionsAll,
 | 
						|
			OwnerID:     repo.OwnerID,
 | 
						|
			IsClosed:    optional.Some(true),
 | 
						|
			Type:        repoOwnerType,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetProjects", err)
 | 
						|
			return nil, nil
 | 
						|
		}
 | 
						|
		closedProjects = append(closedProjects, closedProjects2...)
 | 
						|
	}
 | 
						|
	return openProjects, closedProjects
 | 
						|
}
 | 
						|
 | 
						|
// GetActionIssue will return the issue which is used in the context.
 | 
						|
func GetActionIssue(ctx *context.Context) *issues_model.Issue {
 | 
						|
	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
 | 
						|
	if err != nil {
 | 
						|
		ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	issue.Repo = ctx.Repo.Repository
 | 
						|
	checkIssueRights(ctx, issue)
 | 
						|
	if ctx.Written() {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if err = issue.LoadAttributes(ctx); err != nil {
 | 
						|
		ctx.ServerError("LoadAttributes", err)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	return issue
 | 
						|
}
 | 
						|
 | 
						|
func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
 | 
						|
	if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
 | 
						|
		!issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
 | 
						|
		ctx.NotFound(nil)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func getActionIssues(ctx *context.Context) issues_model.IssueList {
 | 
						|
	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
 | 
						|
	if len(commaSeparatedIssueIDs) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	issueIDs := make([]int64, 0, 10)
 | 
						|
	for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") {
 | 
						|
		issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("ParseInt", err)
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		issueIDs = append(issueIDs, issueID)
 | 
						|
	}
 | 
						|
	issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("GetIssuesByIDs", err)
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	// Check access rights for all issues
 | 
						|
	issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
 | 
						|
	prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
 | 
						|
	for _, issue := range issues {
 | 
						|
		if issue.RepoID != ctx.Repo.Repository.ID {
 | 
						|
			ctx.NotFound(errors.New("some issue's RepoID is incorrect"))
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
 | 
						|
			ctx.NotFound(nil)
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		if err = issue.LoadAttributes(ctx); err != nil {
 | 
						|
			ctx.ServerError("LoadAttributes", err)
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return issues
 | 
						|
}
 | 
						|
 | 
						|
// GetIssueInfo get an issue of a repository
 | 
						|
func GetIssueInfo(ctx *context.Context) {
 | 
						|
	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
 | 
						|
	if err != nil {
 | 
						|
		if issues_model.IsErrIssueNotExist(err) {
 | 
						|
			ctx.HTTPError(http.StatusNotFound)
 | 
						|
		} else {
 | 
						|
			ctx.HTTPError(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if issue.IsPull {
 | 
						|
		// Need to check if Pulls are enabled and we can read Pulls
 | 
						|
		if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
 | 
						|
			ctx.HTTPError(http.StatusNotFound)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		// Need to check if Issues are enabled and we can read Issues
 | 
						|
		if !ctx.Repo.CanRead(unit.TypeIssues) {
 | 
						|
			ctx.HTTPError(http.StatusNotFound)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, map[string]any{
 | 
						|
		"convertedIssue": convert.ToIssue(ctx, ctx.Doer, issue),
 | 
						|
		"renderedLabels": templates.NewRenderUtils(ctx).RenderLabels(issue.Labels, ctx.Repo.RepoLink, issue),
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueTitle change issue's title
 | 
						|
func UpdateIssueTitle(ctx *context.Context) {
 | 
						|
	issue := GetActionIssue(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
 | 
						|
		ctx.HTTPError(http.StatusForbidden)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	title := ctx.FormTrim("title")
 | 
						|
	if len(title) == 0 {
 | 
						|
		ctx.HTTPError(http.StatusNoContent)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := issue_service.ChangeTitle(ctx, issue, ctx.Doer, title); err != nil {
 | 
						|
		ctx.ServerError("ChangeTitle", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, map[string]any{
 | 
						|
		"title": issue.Title,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueRef change issue's ref (branch)
 | 
						|
func UpdateIssueRef(ctx *context.Context) {
 | 
						|
	issue := GetActionIssue(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
 | 
						|
		ctx.HTTPError(http.StatusForbidden)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ref := ctx.FormTrim("ref")
 | 
						|
 | 
						|
	if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil {
 | 
						|
		ctx.ServerError("ChangeRef", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, map[string]any{
 | 
						|
		"ref": ref,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueContent change issue's content
 | 
						|
func UpdateIssueContent(ctx *context.Context) {
 | 
						|
	issue := GetActionIssue(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
 | 
						|
		ctx.HTTPError(http.StatusForbidden)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content"), ctx.FormInt("content_version")); err != nil {
 | 
						|
		if errors.Is(err, user_model.ErrBlockedUser) {
 | 
						|
			ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
 | 
						|
		} else if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
 | 
						|
			if issue.IsPull {
 | 
						|
				ctx.JSONError(ctx.Tr("repo.pulls.edit.already_changed"))
 | 
						|
			} else {
 | 
						|
				ctx.JSONError(ctx.Tr("repo.issues.edit.already_changed"))
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			ctx.ServerError("ChangeContent", err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
 | 
						|
	if !ctx.FormBool("ignore_attachments") {
 | 
						|
		if err := updateAttachments(ctx, issue, ctx.FormStrings("files[]")); err != nil {
 | 
						|
			ctx.ServerError("UpdateAttachments", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
 | 
						|
		FootnoteContextID: "0",
 | 
						|
	})
 | 
						|
	content, err := markdown.RenderString(rctx, issue.Content)
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("RenderString", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, map[string]any{
 | 
						|
		"content":        content,
 | 
						|
		"contentVersion": issue.ContentVersion,
 | 
						|
		"attachments":    attachmentsHTML(ctx, issue.Attachments, issue.Content),
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueDeadline updates an issue deadline
 | 
						|
func UpdateIssueDeadline(ctx *context.Context) {
 | 
						|
	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
 | 
						|
	if err != nil {
 | 
						|
		if issues_model.IsErrIssueNotExist(err) {
 | 
						|
			ctx.NotFound(err)
 | 
						|
		} else {
 | 
						|
			ctx.HTTPError(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
 | 
						|
		ctx.HTTPError(http.StatusForbidden, "", "Not repo writer")
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	deadlineUnix, _ := common.ParseDeadlineDateToEndOfDay(ctx.FormString("deadline"))
 | 
						|
	if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
 | 
						|
		ctx.HTTPError(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSONRedirect("")
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueMilestone change issue's milestone
 | 
						|
func UpdateIssueMilestone(ctx *context.Context) {
 | 
						|
	issues := getActionIssues(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	milestoneID := ctx.FormInt64("id")
 | 
						|
	for _, issue := range issues {
 | 
						|
		oldMilestoneID := issue.MilestoneID
 | 
						|
		if oldMilestoneID == milestoneID {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		issue.MilestoneID = milestoneID
 | 
						|
		if milestoneID > 0 {
 | 
						|
			var err error
 | 
						|
			issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
 | 
						|
			if err != nil {
 | 
						|
				ctx.ServerError("GetMilestoneByRepoID", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			issue.Milestone = nil
 | 
						|
		}
 | 
						|
		if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 | 
						|
			ctx.ServerError("ChangeMilestoneAssign", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSONOK()
 | 
						|
}
 | 
						|
 | 
						|
// UpdateIssueAssignee change issue's or pull's assignee
 | 
						|
func UpdateIssueAssignee(ctx *context.Context) {
 | 
						|
	issues := getActionIssues(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	assigneeID := ctx.FormInt64("id")
 | 
						|
	action := ctx.FormString("action")
 | 
						|
 | 
						|
	for _, issue := range issues {
 | 
						|
		switch action {
 | 
						|
		case "clear":
 | 
						|
			if err := issue_service.DeleteNotPassedAssignee(ctx, issue, ctx.Doer, []*user_model.User{}); err != nil {
 | 
						|
				ctx.ServerError("ClearAssignees", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			assignee, err := user_model.GetUserByID(ctx, assigneeID)
 | 
						|
			if err != nil {
 | 
						|
				ctx.ServerError("GetUserByID", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
 | 
						|
			if err != nil {
 | 
						|
				ctx.ServerError("canBeAssigned", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			if !valid {
 | 
						|
				ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			_, _, err = issue_service.ToggleAssigneeWithNotify(ctx, issue, ctx.Doer, assigneeID)
 | 
						|
			if err != nil {
 | 
						|
				ctx.ServerError("ToggleAssignee", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	ctx.JSONOK()
 | 
						|
}
 | 
						|
 | 
						|
// ChangeIssueReaction create a reaction for issue
 | 
						|
func ChangeIssueReaction(ctx *context.Context) {
 | 
						|
	form := web.GetForm(ctx).(*forms.ReactionForm)
 | 
						|
	issue := GetActionIssue(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
 | 
						|
		if log.IsTrace() {
 | 
						|
			if ctx.IsSigned {
 | 
						|
				issueType := "issues"
 | 
						|
				if issue.IsPull {
 | 
						|
					issueType = "pulls"
 | 
						|
				}
 | 
						|
				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
 | 
						|
					"User in Repo has Permissions: %-+v",
 | 
						|
					ctx.Doer,
 | 
						|
					issue.PosterID,
 | 
						|
					issueType,
 | 
						|
					ctx.Repo.Repository,
 | 
						|
					ctx.Repo.Permission)
 | 
						|
			} else {
 | 
						|
				log.Trace("Permission Denied: Not logged in")
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		ctx.HTTPError(http.StatusForbidden)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if ctx.HasError() {
 | 
						|
		ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	switch ctx.PathParam("action") {
 | 
						|
	case "react":
 | 
						|
		reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
 | 
						|
		if err != nil {
 | 
						|
			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
 | 
						|
				ctx.ServerError("ChangeIssueReaction", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			log.Info("CreateIssueReaction: %s", err)
 | 
						|
			break
 | 
						|
		}
 | 
						|
		// Reload new reactions
 | 
						|
		issue.Reactions = nil
 | 
						|
		if err = issue.LoadAttributes(ctx); err != nil {
 | 
						|
			log.Info("issue.LoadAttributes: %s", err)
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
 | 
						|
	case "unreact":
 | 
						|
		if err := issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content); err != nil {
 | 
						|
			ctx.ServerError("DeleteIssueReaction", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// Reload new reactions
 | 
						|
		issue.Reactions = nil
 | 
						|
		if err := issue.LoadAttributes(ctx); err != nil {
 | 
						|
			log.Info("issue.LoadAttributes: %s", err)
 | 
						|
			break
 | 
						|
		}
 | 
						|
 | 
						|
		log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
 | 
						|
	default:
 | 
						|
		ctx.NotFound(nil)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(issue.Reactions) == 0 {
 | 
						|
		ctx.JSON(http.StatusOK, map[string]any{
 | 
						|
			"empty": true,
 | 
						|
			"html":  "",
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	html, err := ctx.RenderToHTML(tplReactions, map[string]any{
 | 
						|
		"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
 | 
						|
		"Reactions": issue.Reactions.GroupByType(),
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("ChangeIssueReaction.HTMLString", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	ctx.JSON(http.StatusOK, map[string]any{
 | 
						|
		"html": html,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// GetIssueAttachments returns attachments for the issue
 | 
						|
func GetIssueAttachments(ctx *context.Context) {
 | 
						|
	issue := GetActionIssue(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	attachments := make([]*api.Attachment, len(issue.Attachments))
 | 
						|
	for i := 0; i < len(issue.Attachments); i++ {
 | 
						|
		attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i])
 | 
						|
	}
 | 
						|
	ctx.JSON(http.StatusOK, attachments)
 | 
						|
}
 | 
						|
 | 
						|
func updateAttachments(ctx *context.Context, item any, files []string) error {
 | 
						|
	var attachments []*repo_model.Attachment
 | 
						|
	switch content := item.(type) {
 | 
						|
	case *issues_model.Issue:
 | 
						|
		attachments = content.Attachments
 | 
						|
	case *issues_model.Comment:
 | 
						|
		attachments = content.Attachments
 | 
						|
	default:
 | 
						|
		return fmt.Errorf("unknown Type: %T", content)
 | 
						|
	}
 | 
						|
	for i := 0; i < len(attachments); i++ {
 | 
						|
		if util.SliceContainsString(files, attachments[i].UUID) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if err := repo_model.DeleteAttachment(ctx, attachments[i], true); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	var err error
 | 
						|
	if len(files) > 0 {
 | 
						|
		switch content := item.(type) {
 | 
						|
		case *issues_model.Issue:
 | 
						|
			err = issues_model.UpdateIssueAttachments(ctx, content.ID, files)
 | 
						|
		case *issues_model.Comment:
 | 
						|
			err = issues_model.UpdateCommentAttachments(ctx, content, files)
 | 
						|
		default:
 | 
						|
			return fmt.Errorf("unknown Type: %T", content)
 | 
						|
		}
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	switch content := item.(type) {
 | 
						|
	case *issues_model.Issue:
 | 
						|
		content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID)
 | 
						|
	case *issues_model.Comment:
 | 
						|
		content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID)
 | 
						|
	default:
 | 
						|
		return fmt.Errorf("unknown Type: %T", content)
 | 
						|
	}
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
 | 
						|
	attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
 | 
						|
		"ctxData":     ctx.Data,
 | 
						|
		"Attachments": attachments,
 | 
						|
		"Content":     content,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("attachmentsHTML.HTMLString", err)
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	return attachHTML
 | 
						|
}
 | 
						|
 | 
						|
// handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data
 | 
						|
func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) {
 | 
						|
	// TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees"
 | 
						|
	// at the moment it is used on the issue list page, for the markdown editor mention
 | 
						|
	ctx.Data["Assignees"] = assignees
 | 
						|
 | 
						|
	if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var isAdmin bool
 | 
						|
	var err error
 | 
						|
	var teams []*organization.Team
 | 
						|
	org := organization.OrgFromUser(ctx.Repo.Owner)
 | 
						|
	// Admin has super access.
 | 
						|
	if ctx.Doer.IsAdmin {
 | 
						|
		isAdmin = true
 | 
						|
	} else {
 | 
						|
		isAdmin, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("IsOwnedBy", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if isAdmin {
 | 
						|
		teams, err = org.LoadTeams(ctx)
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("LoadTeams", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetUserTeams", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.Data["MentionableTeams"] = teams
 | 
						|
	ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
 | 
						|
	ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
 | 
						|
}
 |