code.gitea.io/gitea@v1.21.7/routers/web/repo/issue.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2018 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"bytes"
     9  	stdCtx "context"
    10  	"errors"
    11  	"fmt"
    12  	"math/big"
    13  	"net/http"
    14  	"net/url"
    15  	"slices"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	activities_model "code.gitea.io/gitea/models/activities"
    22  	"code.gitea.io/gitea/models/db"
    23  	git_model "code.gitea.io/gitea/models/git"
    24  	issues_model "code.gitea.io/gitea/models/issues"
    25  	"code.gitea.io/gitea/models/organization"
    26  	access_model "code.gitea.io/gitea/models/perm/access"
    27  	project_model "code.gitea.io/gitea/models/project"
    28  	pull_model "code.gitea.io/gitea/models/pull"
    29  	repo_model "code.gitea.io/gitea/models/repo"
    30  	"code.gitea.io/gitea/models/unit"
    31  	user_model "code.gitea.io/gitea/models/user"
    32  	"code.gitea.io/gitea/modules/base"
    33  	"code.gitea.io/gitea/modules/container"
    34  	"code.gitea.io/gitea/modules/context"
    35  	"code.gitea.io/gitea/modules/git"
    36  	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
    37  	issue_template "code.gitea.io/gitea/modules/issue/template"
    38  	"code.gitea.io/gitea/modules/log"
    39  	"code.gitea.io/gitea/modules/markup"
    40  	"code.gitea.io/gitea/modules/markup/markdown"
    41  	repo_module "code.gitea.io/gitea/modules/repository"
    42  	"code.gitea.io/gitea/modules/setting"
    43  	api "code.gitea.io/gitea/modules/structs"
    44  	"code.gitea.io/gitea/modules/templates/vars"
    45  	"code.gitea.io/gitea/modules/timeutil"
    46  	"code.gitea.io/gitea/modules/upload"
    47  	"code.gitea.io/gitea/modules/util"
    48  	"code.gitea.io/gitea/modules/web"
    49  	"code.gitea.io/gitea/routers/utils"
    50  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    51  	"code.gitea.io/gitea/services/convert"
    52  	"code.gitea.io/gitea/services/forms"
    53  	issue_service "code.gitea.io/gitea/services/issue"
    54  	pull_service "code.gitea.io/gitea/services/pull"
    55  	repo_service "code.gitea.io/gitea/services/repository"
    56  )
    57  
    58  const (
    59  	tplAttachment base.TplName = "repo/issue/view_content/attachments"
    60  
    61  	tplIssues      base.TplName = "repo/issue/list"
    62  	tplIssueNew    base.TplName = "repo/issue/new"
    63  	tplIssueChoose base.TplName = "repo/issue/choose"
    64  	tplIssueView   base.TplName = "repo/issue/view"
    65  
    66  	tplReactions base.TplName = "repo/issue/view_content/reactions"
    67  
    68  	issueTemplateKey      = "IssueTemplate"
    69  	issueTemplateTitleKey = "IssueTemplateTitle"
    70  )
    71  
    72  // IssueTemplateCandidates issue templates
    73  var IssueTemplateCandidates = []string{
    74  	"ISSUE_TEMPLATE.md",
    75  	"ISSUE_TEMPLATE.yaml",
    76  	"ISSUE_TEMPLATE.yml",
    77  	"issue_template.md",
    78  	"issue_template.yaml",
    79  	"issue_template.yml",
    80  	".gitea/ISSUE_TEMPLATE.md",
    81  	".gitea/ISSUE_TEMPLATE.yaml",
    82  	".gitea/ISSUE_TEMPLATE.yml",
    83  	".gitea/issue_template.md",
    84  	".gitea/issue_template.yaml",
    85  	".gitea/issue_template.yml",
    86  	".github/ISSUE_TEMPLATE.md",
    87  	".github/ISSUE_TEMPLATE.yaml",
    88  	".github/ISSUE_TEMPLATE.yml",
    89  	".github/issue_template.md",
    90  	".github/issue_template.yaml",
    91  	".github/issue_template.yml",
    92  }
    93  
    94  // MustAllowUserComment checks to make sure if an issue is locked.
    95  // If locked and user has permissions to write to the repository,
    96  // then the comment is allowed, else it is blocked
    97  func MustAllowUserComment(ctx *context.Context) {
    98  	issue := GetActionIssue(ctx)
    99  	if ctx.Written() {
   100  		return
   101  	}
   102  
   103  	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
   104  		ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
   105  		ctx.Redirect(issue.Link())
   106  		return
   107  	}
   108  }
   109  
   110  // MustEnableIssues check if repository enable internal issues
   111  func MustEnableIssues(ctx *context.Context) {
   112  	if !ctx.Repo.CanRead(unit.TypeIssues) &&
   113  		!ctx.Repo.CanRead(unit.TypeExternalTracker) {
   114  		ctx.NotFound("MustEnableIssues", nil)
   115  		return
   116  	}
   117  
   118  	unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
   119  	if err == nil {
   120  		ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
   121  		return
   122  	}
   123  }
   124  
   125  // MustAllowPulls check if repository enable pull requests and user have right to do that
   126  func MustAllowPulls(ctx *context.Context) {
   127  	if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
   128  		ctx.NotFound("MustAllowPulls", nil)
   129  		return
   130  	}
   131  
   132  	// User can send pull request if owns a forked repository.
   133  	if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
   134  		ctx.Repo.PullRequest.Allowed = true
   135  		ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
   136  	}
   137  }
   138  
   139  func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
   140  	var err error
   141  	viewType := ctx.FormString("type")
   142  	sortType := ctx.FormString("sort")
   143  	types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested", "reviewed_by"}
   144  	if !util.SliceContainsString(types, viewType, true) {
   145  		viewType = "all"
   146  	}
   147  
   148  	var (
   149  		assigneeID        = ctx.FormInt64("assignee")
   150  		posterID          = ctx.FormInt64("poster")
   151  		mentionedID       int64
   152  		reviewRequestedID int64
   153  		reviewedID        int64
   154  	)
   155  
   156  	if ctx.IsSigned {
   157  		switch viewType {
   158  		case "created_by":
   159  			posterID = ctx.Doer.ID
   160  		case "mentioned":
   161  			mentionedID = ctx.Doer.ID
   162  		case "assigned":
   163  			assigneeID = ctx.Doer.ID
   164  		case "review_requested":
   165  			reviewRequestedID = ctx.Doer.ID
   166  		case "reviewed_by":
   167  			reviewedID = ctx.Doer.ID
   168  		}
   169  	}
   170  
   171  	repo := ctx.Repo.Repository
   172  	var labelIDs []int64
   173  	// 1,-2 means including label 1 and excluding label 2
   174  	// 0 means issues with no label
   175  	// blank means labels will not be filtered for issues
   176  	selectLabels := ctx.FormString("labels")
   177  	if selectLabels == "" {
   178  		ctx.Data["AllLabels"] = true
   179  	} else if selectLabels == "0" {
   180  		ctx.Data["NoLabel"] = true
   181  	}
   182  	if len(selectLabels) > 0 {
   183  		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
   184  		if err != nil {
   185  			ctx.ServerError("StringsToInt64s", err)
   186  			return
   187  		}
   188  	}
   189  
   190  	keyword := strings.Trim(ctx.FormString("q"), " ")
   191  	if bytes.Contains([]byte(keyword), []byte{0x00}) {
   192  		keyword = ""
   193  	}
   194  
   195  	var mileIDs []int64
   196  	if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
   197  		mileIDs = []int64{milestoneID}
   198  	}
   199  
   200  	var issueStats *issues_model.IssueStats
   201  	{
   202  		statsOpts := &issues_model.IssuesOptions{
   203  			RepoIDs:           []int64{repo.ID},
   204  			LabelIDs:          labelIDs,
   205  			MilestoneIDs:      mileIDs,
   206  			ProjectID:         projectID,
   207  			AssigneeID:        assigneeID,
   208  			MentionedID:       mentionedID,
   209  			PosterID:          posterID,
   210  			ReviewRequestedID: reviewRequestedID,
   211  			ReviewedID:        reviewedID,
   212  			IsPull:            isPullOption,
   213  			IssueIDs:          nil,
   214  		}
   215  		if keyword != "" {
   216  			allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
   217  			if err != nil {
   218  				if issue_indexer.IsAvailable(ctx) {
   219  					ctx.ServerError("issueIDsFromSearch", err)
   220  					return
   221  				}
   222  				ctx.Data["IssueIndexerUnavailable"] = true
   223  				return
   224  			}
   225  			statsOpts.IssueIDs = allIssueIDs
   226  		}
   227  		if keyword != "" && len(statsOpts.IssueIDs) == 0 {
   228  			// So it did search with the keyword, but no issue found.
   229  			// Just set issueStats to empty.
   230  			issueStats = &issues_model.IssueStats{}
   231  		} else {
   232  			// So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
   233  			// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
   234  			issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
   235  			if err != nil {
   236  				ctx.ServerError("GetIssueStats", err)
   237  				return
   238  			}
   239  		}
   240  
   241  	}
   242  
   243  	isShowClosed := ctx.FormString("state") == "closed"
   244  	// if open issues are zero and close don't, use closed as default
   245  	if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
   246  		isShowClosed = true
   247  	}
   248  
   249  	page := ctx.FormInt("page")
   250  	if page <= 1 {
   251  		page = 1
   252  	}
   253  
   254  	var total int
   255  	if !isShowClosed {
   256  		total = int(issueStats.OpenCount)
   257  	} else {
   258  		total = int(issueStats.ClosedCount)
   259  	}
   260  	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
   261  
   262  	var issues issues_model.IssueList
   263  	{
   264  		ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{
   265  			Paginator: &db.ListOptions{
   266  				Page:     pager.Paginater.Current(),
   267  				PageSize: setting.UI.IssuePagingNum,
   268  			},
   269  			RepoIDs:           []int64{repo.ID},
   270  			AssigneeID:        assigneeID,
   271  			PosterID:          posterID,
   272  			MentionedID:       mentionedID,
   273  			ReviewRequestedID: reviewRequestedID,
   274  			ReviewedID:        reviewedID,
   275  			MilestoneIDs:      mileIDs,
   276  			ProjectID:         projectID,
   277  			IsClosed:          util.OptionalBoolOf(isShowClosed),
   278  			IsPull:            isPullOption,
   279  			LabelIDs:          labelIDs,
   280  			SortType:          sortType,
   281  		})
   282  		if err != nil {
   283  			if issue_indexer.IsAvailable(ctx) {
   284  				ctx.ServerError("issueIDsFromSearch", err)
   285  				return
   286  			}
   287  			ctx.Data["IssueIndexerUnavailable"] = true
   288  			return
   289  		}
   290  		issues, err = issues_model.GetIssuesByIDs(ctx, ids, true)
   291  		if err != nil {
   292  			ctx.ServerError("GetIssuesByIDs", err)
   293  			return
   294  		}
   295  	}
   296  
   297  	approvalCounts, err := issues.GetApprovalCounts(ctx)
   298  	if err != nil {
   299  		ctx.ServerError("ApprovalCounts", err)
   300  		return
   301  	}
   302  
   303  	// Get posters.
   304  	for i := range issues {
   305  		// Check read status
   306  		if !ctx.IsSigned {
   307  			issues[i].IsRead = true
   308  		} else if err = issues[i].GetIsRead(ctx.Doer.ID); err != nil {
   309  			ctx.ServerError("GetIsRead", err)
   310  			return
   311  		}
   312  	}
   313  
   314  	commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
   315  	if err != nil {
   316  		ctx.ServerError("GetIssuesAllCommitStatus", err)
   317  		return
   318  	}
   319  
   320  	if err := issues.LoadAttributes(ctx); err != nil {
   321  		ctx.ServerError("issues.LoadAttributes", err)
   322  		return
   323  	}
   324  
   325  	ctx.Data["Issues"] = issues
   326  	ctx.Data["CommitLastStatus"] = lastStatus
   327  	ctx.Data["CommitStatuses"] = commitStatuses
   328  
   329  	// Get assignees.
   330  	assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
   331  	if err != nil {
   332  		ctx.ServerError("GetRepoAssignees", err)
   333  		return
   334  	}
   335  	ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
   336  
   337  	handleTeamMentions(ctx)
   338  	if ctx.Written() {
   339  		return
   340  	}
   341  
   342  	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
   343  	if err != nil {
   344  		ctx.ServerError("GetLabelsByRepoID", err)
   345  		return
   346  	}
   347  
   348  	if repo.Owner.IsOrganization() {
   349  		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
   350  		if err != nil {
   351  			ctx.ServerError("GetLabelsByOrgID", err)
   352  			return
   353  		}
   354  
   355  		ctx.Data["OrgLabels"] = orgLabels
   356  		labels = append(labels, orgLabels...)
   357  	}
   358  
   359  	// Get the exclusive scope for every label ID
   360  	labelExclusiveScopes := make([]string, 0, len(labelIDs))
   361  	for _, labelID := range labelIDs {
   362  		foundExclusiveScope := false
   363  		for _, label := range labels {
   364  			if label.ID == labelID || label.ID == -labelID {
   365  				labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
   366  				foundExclusiveScope = true
   367  				break
   368  			}
   369  		}
   370  		if !foundExclusiveScope {
   371  			labelExclusiveScopes = append(labelExclusiveScopes, "")
   372  		}
   373  	}
   374  
   375  	for _, l := range labels {
   376  		l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
   377  	}
   378  	ctx.Data["Labels"] = labels
   379  	ctx.Data["NumLabels"] = len(labels)
   380  
   381  	if ctx.FormInt64("assignee") == 0 {
   382  		assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
   383  	}
   384  
   385  	ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
   386  
   387  	ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
   388  		counts, ok := approvalCounts[issueID]
   389  		if !ok || len(counts) == 0 {
   390  			return 0
   391  		}
   392  		reviewTyp := issues_model.ReviewTypeApprove
   393  		if typ == "reject" {
   394  			reviewTyp = issues_model.ReviewTypeReject
   395  		} else if typ == "waiting" {
   396  			reviewTyp = issues_model.ReviewTypeRequest
   397  		}
   398  		for _, count := range counts {
   399  			if count.Type == reviewTyp {
   400  				return count.Count
   401  			}
   402  		}
   403  		return 0
   404  	}
   405  
   406  	retrieveProjects(ctx, repo)
   407  	if ctx.Written() {
   408  		return
   409  	}
   410  
   411  	pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
   412  	if err != nil {
   413  		ctx.ServerError("GetPinnedIssues", err)
   414  		return
   415  	}
   416  
   417  	ctx.Data["PinnedIssues"] = pinned
   418  	ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
   419  	ctx.Data["IssueStats"] = issueStats
   420  	ctx.Data["SelLabelIDs"] = labelIDs
   421  	ctx.Data["SelectLabels"] = selectLabels
   422  	ctx.Data["ViewType"] = viewType
   423  	ctx.Data["SortType"] = sortType
   424  	ctx.Data["MilestoneID"] = milestoneID
   425  	ctx.Data["ProjectID"] = projectID
   426  	ctx.Data["AssigneeID"] = assigneeID
   427  	ctx.Data["PosterID"] = posterID
   428  	ctx.Data["IsShowClosed"] = isShowClosed
   429  	ctx.Data["Keyword"] = keyword
   430  	if isShowClosed {
   431  		ctx.Data["State"] = "closed"
   432  	} else {
   433  		ctx.Data["State"] = "open"
   434  	}
   435  
   436  	pager.AddParam(ctx, "q", "Keyword")
   437  	pager.AddParam(ctx, "type", "ViewType")
   438  	pager.AddParam(ctx, "sort", "SortType")
   439  	pager.AddParam(ctx, "state", "State")
   440  	pager.AddParam(ctx, "labels", "SelectLabels")
   441  	pager.AddParam(ctx, "milestone", "MilestoneID")
   442  	pager.AddParam(ctx, "project", "ProjectID")
   443  	pager.AddParam(ctx, "assignee", "AssigneeID")
   444  	pager.AddParam(ctx, "poster", "PosterID")
   445  
   446  	if ctx.FormBool("archived") {
   447  		ctx.Data["ShowArchivedLabels"] = true
   448  		pager.AddParam(ctx, "archived", "ShowArchivedLabels")
   449  	}
   450  	ctx.Data["Page"] = pager
   451  }
   452  
   453  func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
   454  	ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
   455  	if err != nil {
   456  		return nil, fmt.Errorf("SearchIssues: %w", err)
   457  	}
   458  	return ids, nil
   459  }
   460  
   461  // Issues render issues page
   462  func Issues(ctx *context.Context) {
   463  	isPullList := ctx.Params(":type") == "pulls"
   464  	if isPullList {
   465  		MustAllowPulls(ctx)
   466  		if ctx.Written() {
   467  			return
   468  		}
   469  		ctx.Data["Title"] = ctx.Tr("repo.pulls")
   470  		ctx.Data["PageIsPullList"] = true
   471  	} else {
   472  		MustEnableIssues(ctx)
   473  		if ctx.Written() {
   474  			return
   475  		}
   476  		ctx.Data["Title"] = ctx.Tr("repo.issues")
   477  		ctx.Data["PageIsIssueList"] = true
   478  		ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
   479  	}
   480  
   481  	issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
   482  	if ctx.Written() {
   483  		return
   484  	}
   485  
   486  	renderMilestones(ctx)
   487  	if ctx.Written() {
   488  		return
   489  	}
   490  
   491  	ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
   492  
   493  	ctx.HTML(http.StatusOK, tplIssues)
   494  }
   495  
   496  func renderMilestones(ctx *context.Context) {
   497  	// Get milestones
   498  	milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
   499  		RepoID: ctx.Repo.Repository.ID,
   500  		State:  api.StateAll,
   501  	})
   502  	if err != nil {
   503  		ctx.ServerError("GetAllRepoMilestones", err)
   504  		return
   505  	}
   506  
   507  	openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
   508  	for _, milestone := range milestones {
   509  		if milestone.IsClosed {
   510  			closedMilestones = append(closedMilestones, milestone)
   511  		} else {
   512  			openMilestones = append(openMilestones, milestone)
   513  		}
   514  	}
   515  	ctx.Data["OpenMilestones"] = openMilestones
   516  	ctx.Data["ClosedMilestones"] = closedMilestones
   517  }
   518  
   519  // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
   520  func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
   521  	var err error
   522  	ctx.Data["OpenMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
   523  		RepoID: repo.ID,
   524  		State:  api.StateOpen,
   525  	})
   526  	if err != nil {
   527  		ctx.ServerError("GetMilestones", err)
   528  		return
   529  	}
   530  	ctx.Data["ClosedMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
   531  		RepoID: repo.ID,
   532  		State:  api.StateClosed,
   533  	})
   534  	if err != nil {
   535  		ctx.ServerError("GetMilestones", err)
   536  		return
   537  	}
   538  
   539  	assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
   540  	if err != nil {
   541  		ctx.ServerError("GetRepoAssignees", err)
   542  		return
   543  	}
   544  	ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
   545  
   546  	handleTeamMentions(ctx)
   547  }
   548  
   549  func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
   550  	// Distinguish whether the owner of the repository
   551  	// is an individual or an organization
   552  	repoOwnerType := project_model.TypeIndividual
   553  	if repo.Owner.IsOrganization() {
   554  		repoOwnerType = project_model.TypeOrganization
   555  	}
   556  	var err error
   557  	projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
   558  		RepoID:   repo.ID,
   559  		Page:     -1,
   560  		IsClosed: util.OptionalBoolFalse,
   561  		Type:     project_model.TypeRepository,
   562  	})
   563  	if err != nil {
   564  		ctx.ServerError("GetProjects", err)
   565  		return
   566  	}
   567  	projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{
   568  		OwnerID:  repo.OwnerID,
   569  		Page:     -1,
   570  		IsClosed: util.OptionalBoolFalse,
   571  		Type:     repoOwnerType,
   572  	})
   573  	if err != nil {
   574  		ctx.ServerError("GetProjects", err)
   575  		return
   576  	}
   577  
   578  	ctx.Data["OpenProjects"] = append(projects, projects2...)
   579  
   580  	projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
   581  		RepoID:   repo.ID,
   582  		Page:     -1,
   583  		IsClosed: util.OptionalBoolTrue,
   584  		Type:     project_model.TypeRepository,
   585  	})
   586  	if err != nil {
   587  		ctx.ServerError("GetProjects", err)
   588  		return
   589  	}
   590  	projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{
   591  		OwnerID:  repo.OwnerID,
   592  		Page:     -1,
   593  		IsClosed: util.OptionalBoolTrue,
   594  		Type:     repoOwnerType,
   595  	})
   596  	if err != nil {
   597  		ctx.ServerError("GetProjects", err)
   598  		return
   599  	}
   600  
   601  	ctx.Data["ClosedProjects"] = append(projects, projects2...)
   602  }
   603  
   604  // repoReviewerSelection items to bee shown
   605  type repoReviewerSelection struct {
   606  	IsTeam    bool
   607  	Team      *organization.Team
   608  	User      *user_model.User
   609  	Review    *issues_model.Review
   610  	CanChange bool
   611  	Checked   bool
   612  	ItemID    int64
   613  }
   614  
   615  // RetrieveRepoReviewers find all reviewers of a repository
   616  func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
   617  	ctx.Data["CanChooseReviewer"] = canChooseReviewer
   618  
   619  	originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID)
   620  	if err != nil {
   621  		ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
   622  		return
   623  	}
   624  	ctx.Data["OriginalReviews"] = originalAuthorReviews
   625  
   626  	reviews, err := issues_model.GetReviewsByIssueID(ctx, issue.ID)
   627  	if err != nil {
   628  		ctx.ServerError("GetReviewersByIssueID", err)
   629  		return
   630  	}
   631  
   632  	if len(reviews) == 0 && !canChooseReviewer {
   633  		return
   634  	}
   635  
   636  	var (
   637  		pullReviews         []*repoReviewerSelection
   638  		reviewersResult     []*repoReviewerSelection
   639  		teamReviewersResult []*repoReviewerSelection
   640  		teamReviewers       []*organization.Team
   641  		reviewers           []*user_model.User
   642  	)
   643  
   644  	if canChooseReviewer {
   645  		posterID := issue.PosterID
   646  		if issue.OriginalAuthorID > 0 {
   647  			posterID = 0
   648  		}
   649  
   650  		reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
   651  		if err != nil {
   652  			ctx.ServerError("GetReviewers", err)
   653  			return
   654  		}
   655  
   656  		teamReviewers, err = repo_service.GetReviewerTeams(ctx, repo)
   657  		if err != nil {
   658  			ctx.ServerError("GetReviewerTeams", err)
   659  			return
   660  		}
   661  
   662  		if len(reviewers) > 0 {
   663  			reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
   664  		}
   665  
   666  		if len(teamReviewers) > 0 {
   667  			teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
   668  		}
   669  	}
   670  
   671  	pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
   672  
   673  	for _, review := range reviews {
   674  		tmp := &repoReviewerSelection{
   675  			Checked: review.Type == issues_model.ReviewTypeRequest,
   676  			Review:  review,
   677  			ItemID:  review.ReviewerID,
   678  		}
   679  		if review.ReviewerTeamID > 0 {
   680  			tmp.IsTeam = true
   681  			tmp.ItemID = -review.ReviewerTeamID
   682  		}
   683  
   684  		if ctx.Repo.IsAdmin() {
   685  			// Admin can dismiss or re-request any review requests
   686  			tmp.CanChange = true
   687  		} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
   688  			// A user can refuse review requests
   689  			tmp.CanChange = true
   690  		} else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
   691  			ctx.Doer.ID != review.ReviewerID {
   692  			// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
   693  			tmp.CanChange = true
   694  		}
   695  
   696  		pullReviews = append(pullReviews, tmp)
   697  
   698  		if canChooseReviewer {
   699  			if tmp.IsTeam {
   700  				teamReviewersResult = append(teamReviewersResult, tmp)
   701  			} else {
   702  				reviewersResult = append(reviewersResult, tmp)
   703  			}
   704  		}
   705  	}
   706  
   707  	if len(pullReviews) > 0 {
   708  		// Drop all non-existing users and teams from the reviews
   709  		currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
   710  		for _, item := range pullReviews {
   711  			if item.Review.ReviewerID > 0 {
   712  				if err = item.Review.LoadReviewer(ctx); err != nil {
   713  					if user_model.IsErrUserNotExist(err) {
   714  						continue
   715  					}
   716  					ctx.ServerError("LoadReviewer", err)
   717  					return
   718  				}
   719  				item.User = item.Review.Reviewer
   720  			} else if item.Review.ReviewerTeamID > 0 {
   721  				if err = item.Review.LoadReviewerTeam(ctx); err != nil {
   722  					if organization.IsErrTeamNotExist(err) {
   723  						continue
   724  					}
   725  					ctx.ServerError("LoadReviewerTeam", err)
   726  					return
   727  				}
   728  				item.Team = item.Review.ReviewerTeam
   729  			} else {
   730  				continue
   731  			}
   732  
   733  			currentPullReviewers = append(currentPullReviewers, item)
   734  		}
   735  		ctx.Data["PullReviewers"] = currentPullReviewers
   736  	}
   737  
   738  	if canChooseReviewer && reviewersResult != nil {
   739  		preadded := len(reviewersResult)
   740  		for _, reviewer := range reviewers {
   741  			found := false
   742  		reviewAddLoop:
   743  			for _, tmp := range reviewersResult[:preadded] {
   744  				if tmp.ItemID == reviewer.ID {
   745  					tmp.User = reviewer
   746  					found = true
   747  					break reviewAddLoop
   748  				}
   749  			}
   750  
   751  			if found {
   752  				continue
   753  			}
   754  
   755  			reviewersResult = append(reviewersResult, &repoReviewerSelection{
   756  				IsTeam:    false,
   757  				CanChange: true,
   758  				User:      reviewer,
   759  				ItemID:    reviewer.ID,
   760  			})
   761  		}
   762  
   763  		ctx.Data["Reviewers"] = reviewersResult
   764  	}
   765  
   766  	if canChooseReviewer && teamReviewersResult != nil {
   767  		preadded := len(teamReviewersResult)
   768  		for _, team := range teamReviewers {
   769  			found := false
   770  		teamReviewAddLoop:
   771  			for _, tmp := range teamReviewersResult[:preadded] {
   772  				if tmp.ItemID == -team.ID {
   773  					tmp.Team = team
   774  					found = true
   775  					break teamReviewAddLoop
   776  				}
   777  			}
   778  
   779  			if found {
   780  				continue
   781  			}
   782  
   783  			teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
   784  				IsTeam:    true,
   785  				CanChange: true,
   786  				Team:      team,
   787  				ItemID:    -team.ID,
   788  			})
   789  		}
   790  
   791  		ctx.Data["TeamReviewers"] = teamReviewersResult
   792  	}
   793  }
   794  
   795  // RetrieveRepoMetas find all the meta information of a repository
   796  func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
   797  	if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
   798  		return nil
   799  	}
   800  
   801  	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
   802  	if err != nil {
   803  		ctx.ServerError("GetLabelsByRepoID", err)
   804  		return nil
   805  	}
   806  	ctx.Data["Labels"] = labels
   807  	if repo.Owner.IsOrganization() {
   808  		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
   809  		if err != nil {
   810  			return nil
   811  		}
   812  
   813  		ctx.Data["OrgLabels"] = orgLabels
   814  		labels = append(labels, orgLabels...)
   815  	}
   816  
   817  	RetrieveRepoMilestonesAndAssignees(ctx, repo)
   818  	if ctx.Written() {
   819  		return nil
   820  	}
   821  
   822  	retrieveProjects(ctx, repo)
   823  	if ctx.Written() {
   824  		return nil
   825  	}
   826  
   827  	PrepareBranchList(ctx)
   828  	if ctx.Written() {
   829  		return nil
   830  	}
   831  
   832  	// Contains true if the user can create issue dependencies
   833  	ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull)
   834  
   835  	return labels
   836  }
   837  
   838  // Tries to load and set an issue template. The first return value indicates if a template was loaded.
   839  func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
   840  	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
   841  	if err != nil {
   842  		return false, nil
   843  	}
   844  
   845  	templateCandidates := make([]string, 0, 1+len(possibleFiles))
   846  	if t := ctx.FormString("template"); t != "" {
   847  		templateCandidates = append(templateCandidates, t)
   848  	}
   849  	templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
   850  
   851  	templateErrs := map[string]error{}
   852  	for _, filename := range templateCandidates {
   853  		if ok, _ := commit.HasFile(filename); !ok {
   854  			continue
   855  		}
   856  		template, err := issue_template.UnmarshalFromCommit(commit, filename)
   857  		if err != nil {
   858  			templateErrs[filename] = err
   859  			continue
   860  		}
   861  		ctx.Data[issueTemplateTitleKey] = template.Title
   862  		ctx.Data[ctxDataKey] = template.Content
   863  
   864  		if template.Type() == api.IssueTemplateTypeYaml {
   865  			// Replace field default values by values from query
   866  			for _, field := range template.Fields {
   867  				fieldValue := ctx.FormString("field:" + field.ID)
   868  				if fieldValue != "" {
   869  					field.Attributes["value"] = fieldValue
   870  				}
   871  			}
   872  
   873  			ctx.Data["Fields"] = template.Fields
   874  			ctx.Data["TemplateFile"] = template.FileName
   875  		}
   876  		labelIDs := make([]string, 0, len(template.Labels))
   877  		if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
   878  			ctx.Data["Labels"] = repoLabels
   879  			if ctx.Repo.Owner.IsOrganization() {
   880  				if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
   881  					ctx.Data["OrgLabels"] = orgLabels
   882  					repoLabels = append(repoLabels, orgLabels...)
   883  				}
   884  			}
   885  
   886  			for _, metaLabel := range template.Labels {
   887  				for _, repoLabel := range repoLabels {
   888  					if strings.EqualFold(repoLabel.Name, metaLabel) {
   889  						repoLabel.IsChecked = true
   890  						labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
   891  						break
   892  					}
   893  				}
   894  			}
   895  
   896  		}
   897  
   898  		if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
   899  			template.Ref = git.BranchPrefix + template.Ref
   900  		}
   901  		ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
   902  		ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
   903  		ctx.Data["Reference"] = template.Ref
   904  		ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
   905  		return true, templateErrs
   906  	}
   907  	return false, templateErrs
   908  }
   909  
   910  // NewIssue render creating issue page
   911  func NewIssue(ctx *context.Context) {
   912  	issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
   913  	hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
   914  
   915  	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
   916  	ctx.Data["PageIsIssueList"] = true
   917  	ctx.Data["NewIssueChooseTemplate"] = hasTemplates
   918  	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
   919  	title := ctx.FormString("title")
   920  	ctx.Data["TitleQuery"] = title
   921  	body := ctx.FormString("body")
   922  	ctx.Data["BodyQuery"] = body
   923  
   924  	isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects)
   925  	ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
   926  	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
   927  	upload.AddUploadContext(ctx, "comment")
   928  
   929  	milestoneID := ctx.FormInt64("milestone")
   930  	if milestoneID > 0 {
   931  		milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
   932  		if err != nil {
   933  			log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
   934  		} else {
   935  			ctx.Data["milestone_id"] = milestoneID
   936  			ctx.Data["Milestone"] = milestone
   937  		}
   938  	}
   939  
   940  	projectID := ctx.FormInt64("project")
   941  	if projectID > 0 && isProjectsEnabled {
   942  		project, err := project_model.GetProjectByID(ctx, projectID)
   943  		if err != nil {
   944  			log.Error("GetProjectByID: %d: %v", projectID, err)
   945  		} else if project.RepoID != ctx.Repo.Repository.ID {
   946  			log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
   947  		} else {
   948  			ctx.Data["project_id"] = projectID
   949  			ctx.Data["Project"] = project
   950  		}
   951  
   952  		if len(ctx.Req.URL.Query().Get("project")) > 0 {
   953  			ctx.Data["redirect_after_creation"] = "project"
   954  		}
   955  	}
   956  
   957  	RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
   958  
   959  	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
   960  	if err != nil {
   961  		ctx.ServerError("GetTagNamesByRepoID", err)
   962  		return
   963  	}
   964  	ctx.Data["Tags"] = tags
   965  
   966  	ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
   967  	templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
   968  	for k, v := range errs {
   969  		ret.TemplateErrors[k] = v
   970  	}
   971  	if ctx.Written() {
   972  		return
   973  	}
   974  
   975  	if len(ret.TemplateErrors) > 0 {
   976  		ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
   977  	}
   978  
   979  	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
   980  
   981  	if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded {
   982  		// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters.
   983  		ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
   984  		return
   985  	}
   986  
   987  	ctx.HTML(http.StatusOK, tplIssueNew)
   988  }
   989  
   990  func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
   991  	var files []string
   992  	for k := range errs {
   993  		files = append(files, k)
   994  	}
   995  	sort.Strings(files) // keep the output stable
   996  
   997  	var lines []string
   998  	for _, file := range files {
   999  		lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
  1000  	}
  1001  
  1002  	flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1003  		"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
  1004  		"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
  1005  		"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
  1006  	})
  1007  	if err != nil {
  1008  		log.Debug("render flash error: %v", err)
  1009  		flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
  1010  	}
  1011  	return flashError
  1012  }
  1013  
  1014  // NewIssueChooseTemplate render creating issue from template page
  1015  func NewIssueChooseTemplate(ctx *context.Context) {
  1016  	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  1017  	ctx.Data["PageIsIssueList"] = true
  1018  
  1019  	ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1020  	ctx.Data["IssueTemplates"] = ret.IssueTemplates
  1021  
  1022  	if len(ret.TemplateErrors) > 0 {
  1023  		ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
  1024  	}
  1025  
  1026  	if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
  1027  		// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
  1028  		ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
  1029  		return
  1030  	}
  1031  
  1032  	issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1033  	ctx.Data["IssueConfig"] = issueConfig
  1034  	ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here
  1035  
  1036  	ctx.Data["milestone"] = ctx.FormInt64("milestone")
  1037  	ctx.Data["project"] = ctx.FormInt64("project")
  1038  
  1039  	ctx.HTML(http.StatusOK, tplIssueChoose)
  1040  }
  1041  
  1042  // DeleteIssue deletes an issue
  1043  func DeleteIssue(ctx *context.Context) {
  1044  	issue := GetActionIssue(ctx)
  1045  	if ctx.Written() {
  1046  		return
  1047  	}
  1048  
  1049  	if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
  1050  		ctx.ServerError("DeleteIssueByID", err)
  1051  		return
  1052  	}
  1053  
  1054  	if issue.IsPull {
  1055  		ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther)
  1056  		return
  1057  	}
  1058  
  1059  	ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
  1060  }
  1061  
  1062  // ValidateRepoMetas check and returns repository's meta information
  1063  func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
  1064  	var (
  1065  		repo = ctx.Repo.Repository
  1066  		err  error
  1067  	)
  1068  
  1069  	labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
  1070  	if ctx.Written() {
  1071  		return nil, nil, 0, 0
  1072  	}
  1073  
  1074  	var labelIDs []int64
  1075  	hasSelected := false
  1076  	// Check labels.
  1077  	if len(form.LabelIDs) > 0 {
  1078  		labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
  1079  		if err != nil {
  1080  			return nil, nil, 0, 0
  1081  		}
  1082  		labelIDMark := make(container.Set[int64])
  1083  		labelIDMark.AddMultiple(labelIDs...)
  1084  
  1085  		for i := range labels {
  1086  			if labelIDMark.Contains(labels[i].ID) {
  1087  				labels[i].IsChecked = true
  1088  				hasSelected = true
  1089  			}
  1090  		}
  1091  	}
  1092  
  1093  	ctx.Data["Labels"] = labels
  1094  	ctx.Data["HasSelectedLabel"] = hasSelected
  1095  	ctx.Data["label_ids"] = form.LabelIDs
  1096  
  1097  	// Check milestone.
  1098  	milestoneID := form.MilestoneID
  1099  	if milestoneID > 0 {
  1100  		milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
  1101  		if err != nil {
  1102  			ctx.ServerError("GetMilestoneByID", err)
  1103  			return nil, nil, 0, 0
  1104  		}
  1105  		if milestone.RepoID != repo.ID {
  1106  			ctx.ServerError("GetMilestoneByID", err)
  1107  			return nil, nil, 0, 0
  1108  		}
  1109  		ctx.Data["Milestone"] = milestone
  1110  		ctx.Data["milestone_id"] = milestoneID
  1111  	}
  1112  
  1113  	if form.ProjectID > 0 {
  1114  		p, err := project_model.GetProjectByID(ctx, form.ProjectID)
  1115  		if err != nil {
  1116  			ctx.ServerError("GetProjectByID", err)
  1117  			return nil, nil, 0, 0
  1118  		}
  1119  		if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID {
  1120  			ctx.NotFound("", nil)
  1121  			return nil, nil, 0, 0
  1122  		}
  1123  
  1124  		ctx.Data["Project"] = p
  1125  		ctx.Data["project_id"] = form.ProjectID
  1126  	}
  1127  
  1128  	// Check assignees
  1129  	var assigneeIDs []int64
  1130  	if len(form.AssigneeIDs) > 0 {
  1131  		assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
  1132  		if err != nil {
  1133  			return nil, nil, 0, 0
  1134  		}
  1135  
  1136  		// Check if the passed assignees actually exists and is assignable
  1137  		for _, aID := range assigneeIDs {
  1138  			assignee, err := user_model.GetUserByID(ctx, aID)
  1139  			if err != nil {
  1140  				ctx.ServerError("GetUserByID", err)
  1141  				return nil, nil, 0, 0
  1142  			}
  1143  
  1144  			valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
  1145  			if err != nil {
  1146  				ctx.ServerError("CanBeAssigned", err)
  1147  				return nil, nil, 0, 0
  1148  			}
  1149  
  1150  			if !valid {
  1151  				ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
  1152  				return nil, nil, 0, 0
  1153  			}
  1154  		}
  1155  	}
  1156  
  1157  	// Keep the old assignee id thingy for compatibility reasons
  1158  	if form.AssigneeID > 0 {
  1159  		assigneeIDs = append(assigneeIDs, form.AssigneeID)
  1160  	}
  1161  
  1162  	return labelIDs, assigneeIDs, milestoneID, form.ProjectID
  1163  }
  1164  
  1165  // NewIssuePost response for creating new issue
  1166  func NewIssuePost(ctx *context.Context) {
  1167  	form := web.GetForm(ctx).(*forms.CreateIssueForm)
  1168  	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  1169  	ctx.Data["PageIsIssueList"] = true
  1170  	ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1171  	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
  1172  	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  1173  	upload.AddUploadContext(ctx, "comment")
  1174  
  1175  	var (
  1176  		repo        = ctx.Repo.Repository
  1177  		attachments []string
  1178  	)
  1179  
  1180  	labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
  1181  	if ctx.Written() {
  1182  		return
  1183  	}
  1184  
  1185  	if setting.Attachment.Enabled {
  1186  		attachments = form.Files
  1187  	}
  1188  
  1189  	if ctx.HasError() {
  1190  		ctx.JSONError(ctx.GetErrMsg())
  1191  		return
  1192  	}
  1193  
  1194  	if util.IsEmptyString(form.Title) {
  1195  		ctx.JSONError(ctx.Tr("repo.issues.new.title_empty"))
  1196  		return
  1197  	}
  1198  
  1199  	content := form.Content
  1200  	if filename := ctx.Req.Form.Get("template-file"); filename != "" {
  1201  		if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
  1202  			content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
  1203  		}
  1204  	}
  1205  
  1206  	issue := &issues_model.Issue{
  1207  		RepoID:      repo.ID,
  1208  		Repo:        repo,
  1209  		Title:       form.Title,
  1210  		PosterID:    ctx.Doer.ID,
  1211  		Poster:      ctx.Doer,
  1212  		MilestoneID: milestoneID,
  1213  		Content:     content,
  1214  		Ref:         form.Ref,
  1215  	}
  1216  
  1217  	if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
  1218  		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
  1219  			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
  1220  			return
  1221  		}
  1222  		ctx.ServerError("NewIssue", err)
  1223  		return
  1224  	}
  1225  
  1226  	if projectID > 0 {
  1227  		if !ctx.Repo.CanRead(unit.TypeProjects) {
  1228  			// User must also be able to see the project.
  1229  			ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
  1230  			return
  1231  		}
  1232  		if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
  1233  			ctx.ServerError("ChangeProjectAssign", err)
  1234  			return
  1235  		}
  1236  	}
  1237  
  1238  	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
  1239  	if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
  1240  		ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
  1241  	} else {
  1242  		ctx.JSONRedirect(issue.Link())
  1243  	}
  1244  }
  1245  
  1246  // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
  1247  func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) {
  1248  	roleDescriptor := issues_model.RoleDescriptor{}
  1249  
  1250  	if hasOriginalAuthor {
  1251  		return roleDescriptor, nil
  1252  	}
  1253  
  1254  	perm, err := access_model.GetUserRepoPermission(ctx, repo, poster)
  1255  	if err != nil {
  1256  		return roleDescriptor, err
  1257  	}
  1258  
  1259  	// If the poster is the actual poster of the issue, enable Poster role.
  1260  	roleDescriptor.IsPoster = issue.IsPoster(poster.ID)
  1261  
  1262  	// Check if the poster is owner of the repo.
  1263  	if perm.IsOwner() {
  1264  		// If the poster isn't an admin, enable the owner role.
  1265  		if !poster.IsAdmin {
  1266  			roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
  1267  			return roleDescriptor, nil
  1268  		}
  1269  
  1270  		// Otherwise check if poster is the real repo admin.
  1271  		ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
  1272  		if err != nil {
  1273  			return roleDescriptor, err
  1274  		}
  1275  		if ok {
  1276  			roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
  1277  			return roleDescriptor, nil
  1278  		}
  1279  	}
  1280  
  1281  	// If repo is organization, check Member role
  1282  	if err := repo.LoadOwner(ctx); err != nil {
  1283  		return roleDescriptor, err
  1284  	}
  1285  	if repo.Owner.IsOrganization() {
  1286  		if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
  1287  			return roleDescriptor, err
  1288  		} else if isMember {
  1289  			roleDescriptor.RoleInRepo = issues_model.RoleRepoMember
  1290  			return roleDescriptor, nil
  1291  		}
  1292  	}
  1293  
  1294  	// If the poster is the collaborator of the repo
  1295  	if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
  1296  		return roleDescriptor, err
  1297  	} else if isCollaborator {
  1298  		roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator
  1299  		return roleDescriptor, nil
  1300  	}
  1301  
  1302  	hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
  1303  	if err != nil {
  1304  		return roleDescriptor, err
  1305  	} else if hasMergedPR {
  1306  		roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
  1307  	} else if issue.IsPull {
  1308  		// only display first time contributor in the first opening pull request
  1309  		roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
  1310  	}
  1311  
  1312  	return roleDescriptor, nil
  1313  }
  1314  
  1315  func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
  1316  	ctx.Data["BaseBranch"] = nil
  1317  	ctx.Data["HeadBranch"] = nil
  1318  	ctx.Data["HeadUserName"] = nil
  1319  	ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
  1320  	if issue.IsPull {
  1321  		pull := issue.PullRequest
  1322  		ctx.Data["BaseBranch"] = pull.BaseBranch
  1323  		ctx.Data["HeadBranch"] = pull.HeadBranch
  1324  		ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx)
  1325  	}
  1326  }
  1327  
  1328  // ViewIssue render issue view page
  1329  func ViewIssue(ctx *context.Context) {
  1330  	if ctx.Params(":type") == "issues" {
  1331  		// If issue was requested we check if repo has external tracker and redirect
  1332  		extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
  1333  		if err == nil && extIssueUnit != nil {
  1334  			if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
  1335  				metas := ctx.Repo.Repository.ComposeMetas()
  1336  				metas["index"] = ctx.Params(":index")
  1337  				res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
  1338  				if err != nil {
  1339  					log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err)
  1340  					ctx.ServerError("Expand", err)
  1341  					return
  1342  				}
  1343  				ctx.Redirect(res)
  1344  				return
  1345  			}
  1346  		} else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
  1347  			ctx.ServerError("GetUnit", err)
  1348  			return
  1349  		}
  1350  	}
  1351  
  1352  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1353  	if err != nil {
  1354  		if issues_model.IsErrIssueNotExist(err) {
  1355  			ctx.NotFound("GetIssueByIndex", err)
  1356  		} else {
  1357  			ctx.ServerError("GetIssueByIndex", err)
  1358  		}
  1359  		return
  1360  	}
  1361  	if issue.Repo == nil {
  1362  		issue.Repo = ctx.Repo.Repository
  1363  	}
  1364  
  1365  	// Make sure type and URL matches.
  1366  	if ctx.Params(":type") == "issues" && issue.IsPull {
  1367  		ctx.Redirect(issue.Link())
  1368  		return
  1369  	} else if ctx.Params(":type") == "pulls" && !issue.IsPull {
  1370  		ctx.Redirect(issue.Link())
  1371  		return
  1372  	}
  1373  
  1374  	if issue.IsPull {
  1375  		MustAllowPulls(ctx)
  1376  		if ctx.Written() {
  1377  			return
  1378  		}
  1379  		ctx.Data["PageIsPullList"] = true
  1380  		ctx.Data["PageIsPullConversation"] = true
  1381  	} else {
  1382  		MustEnableIssues(ctx)
  1383  		if ctx.Written() {
  1384  			return
  1385  		}
  1386  		ctx.Data["PageIsIssueList"] = true
  1387  		ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  1388  	}
  1389  
  1390  	if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
  1391  		ctx.Data["IssueType"] = "pulls"
  1392  	} else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
  1393  		ctx.Data["IssueType"] = "issues"
  1394  	} else {
  1395  		ctx.Data["IssueType"] = "all"
  1396  	}
  1397  
  1398  	ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
  1399  	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  1400  	upload.AddUploadContext(ctx, "comment")
  1401  
  1402  	if err = issue.LoadAttributes(ctx); err != nil {
  1403  		ctx.ServerError("LoadAttributes", err)
  1404  		return
  1405  	}
  1406  
  1407  	if err = filterXRefComments(ctx, issue); err != nil {
  1408  		ctx.ServerError("filterXRefComments", err)
  1409  		return
  1410  	}
  1411  
  1412  	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
  1413  
  1414  	iw := new(issues_model.IssueWatch)
  1415  	if ctx.Doer != nil {
  1416  		iw.UserID = ctx.Doer.ID
  1417  		iw.IssueID = issue.ID
  1418  		iw.IsWatching, err = issues_model.CheckIssueWatch(ctx, ctx.Doer, issue)
  1419  		if err != nil {
  1420  			ctx.ServerError("CheckIssueWatch", err)
  1421  			return
  1422  		}
  1423  	}
  1424  	ctx.Data["IssueWatch"] = iw
  1425  	issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1426  		Links: markup.Links{
  1427  			Base: ctx.Repo.RepoLink,
  1428  		},
  1429  		Metas:   ctx.Repo.Repository.ComposeMetas(),
  1430  		GitRepo: ctx.Repo.GitRepo,
  1431  		Ctx:     ctx,
  1432  	}, issue.Content)
  1433  	if err != nil {
  1434  		ctx.ServerError("RenderString", err)
  1435  		return
  1436  	}
  1437  
  1438  	repo := ctx.Repo.Repository
  1439  
  1440  	// Get more information if it's a pull request.
  1441  	if issue.IsPull {
  1442  		if issue.PullRequest.HasMerged {
  1443  			ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
  1444  			PrepareMergedViewPullInfo(ctx, issue)
  1445  		} else {
  1446  			PrepareViewPullInfo(ctx, issue)
  1447  			ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
  1448  		}
  1449  		if ctx.Written() {
  1450  			return
  1451  		}
  1452  	}
  1453  
  1454  	// Metas.
  1455  	// Check labels.
  1456  	labelIDMark := make(container.Set[int64])
  1457  	for _, label := range issue.Labels {
  1458  		labelIDMark.Add(label.ID)
  1459  	}
  1460  	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
  1461  	if err != nil {
  1462  		ctx.ServerError("GetLabelsByRepoID", err)
  1463  		return
  1464  	}
  1465  	ctx.Data["Labels"] = labels
  1466  
  1467  	if repo.Owner.IsOrganization() {
  1468  		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
  1469  		if err != nil {
  1470  			ctx.ServerError("GetLabelsByOrgID", err)
  1471  			return
  1472  		}
  1473  		ctx.Data["OrgLabels"] = orgLabels
  1474  
  1475  		labels = append(labels, orgLabels...)
  1476  	}
  1477  
  1478  	hasSelected := false
  1479  	for i := range labels {
  1480  		if labelIDMark.Contains(labels[i].ID) {
  1481  			labels[i].IsChecked = true
  1482  			hasSelected = true
  1483  		}
  1484  	}
  1485  	ctx.Data["HasSelectedLabel"] = hasSelected
  1486  
  1487  	// Check milestone and assignee.
  1488  	if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  1489  		RetrieveRepoMilestonesAndAssignees(ctx, repo)
  1490  		retrieveProjects(ctx, repo)
  1491  
  1492  		if ctx.Written() {
  1493  			return
  1494  		}
  1495  	}
  1496  
  1497  	if issue.IsPull {
  1498  		canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
  1499  		if ctx.Doer != nil && ctx.IsSigned {
  1500  			if !canChooseReviewer {
  1501  				canChooseReviewer = ctx.Doer.ID == issue.PosterID
  1502  			}
  1503  			if !canChooseReviewer {
  1504  				canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
  1505  				if err != nil {
  1506  					ctx.ServerError("IsOfficialReviewer", err)
  1507  					return
  1508  				}
  1509  			}
  1510  		}
  1511  
  1512  		RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
  1513  		if ctx.Written() {
  1514  			return
  1515  		}
  1516  	}
  1517  
  1518  	if ctx.IsSigned {
  1519  		// Update issue-user.
  1520  		if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
  1521  			ctx.ServerError("ReadBy", err)
  1522  			return
  1523  		}
  1524  	}
  1525  
  1526  	var (
  1527  		role                 issues_model.RoleDescriptor
  1528  		ok                   bool
  1529  		marked               = make(map[int64]issues_model.RoleDescriptor)
  1530  		comment              *issues_model.Comment
  1531  		participants         = make([]*user_model.User, 1, 10)
  1532  		latestCloseCommentID int64
  1533  	)
  1534  	if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
  1535  		if ctx.IsSigned {
  1536  			// Deal with the stopwatch
  1537  			ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID)
  1538  			if !ctx.Data["IsStopwatchRunning"].(bool) {
  1539  				var exists bool
  1540  				var swIssue *issues_model.Issue
  1541  				if exists, _, swIssue, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil {
  1542  					ctx.ServerError("HasUserStopwatch", err)
  1543  					return
  1544  				}
  1545  				ctx.Data["HasUserStopwatch"] = exists
  1546  				if exists {
  1547  					// Add warning if the user has already a stopwatch
  1548  					// Add link to the issue of the already running stopwatch
  1549  					ctx.Data["OtherStopwatchURL"] = swIssue.Link()
  1550  				}
  1551  			}
  1552  			ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.Doer)
  1553  		} else {
  1554  			ctx.Data["CanUseTimetracker"] = false
  1555  		}
  1556  		if ctx.Data["WorkingUsers"], err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
  1557  			ctx.ServerError("TotalTimes", err)
  1558  			return
  1559  		}
  1560  	}
  1561  
  1562  	// Check if the user can use the dependencies
  1563  	ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, issue.IsPull)
  1564  
  1565  	// check if dependencies can be created across repositories
  1566  	ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
  1567  
  1568  	if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil {
  1569  		ctx.ServerError("roleDescriptor", err)
  1570  		return
  1571  	}
  1572  	marked[issue.PosterID] = issue.ShowRole
  1573  
  1574  	// Render comments and and fetch participants.
  1575  	participants[0] = issue.Poster
  1576  	for _, comment = range issue.Comments {
  1577  		comment.Issue = issue
  1578  
  1579  		if err := comment.LoadPoster(ctx); err != nil {
  1580  			ctx.ServerError("LoadPoster", err)
  1581  			return
  1582  		}
  1583  
  1584  		if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
  1585  			if err := comment.LoadAttachments(ctx); err != nil {
  1586  				ctx.ServerError("LoadAttachments", err)
  1587  				return
  1588  			}
  1589  
  1590  			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1591  				Links: markup.Links{
  1592  					Base: ctx.Repo.RepoLink,
  1593  				},
  1594  				Metas:   ctx.Repo.Repository.ComposeMetas(),
  1595  				GitRepo: ctx.Repo.GitRepo,
  1596  				Ctx:     ctx,
  1597  			}, comment.Content)
  1598  			if err != nil {
  1599  				ctx.ServerError("RenderString", err)
  1600  				return
  1601  			}
  1602  			// Check tag.
  1603  			role, ok = marked[comment.PosterID]
  1604  			if ok {
  1605  				comment.ShowRole = role
  1606  				continue
  1607  			}
  1608  
  1609  			comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor())
  1610  			if err != nil {
  1611  				ctx.ServerError("roleDescriptor", err)
  1612  				return
  1613  			}
  1614  			marked[comment.PosterID] = comment.ShowRole
  1615  			participants = addParticipant(comment.Poster, participants)
  1616  		} else if comment.Type == issues_model.CommentTypeLabel {
  1617  			if err = comment.LoadLabel(ctx); err != nil {
  1618  				ctx.ServerError("LoadLabel", err)
  1619  				return
  1620  			}
  1621  		} else if comment.Type == issues_model.CommentTypeMilestone {
  1622  			if err = comment.LoadMilestone(ctx); err != nil {
  1623  				ctx.ServerError("LoadMilestone", err)
  1624  				return
  1625  			}
  1626  			ghostMilestone := &issues_model.Milestone{
  1627  				ID:   -1,
  1628  				Name: ctx.Tr("repo.issues.deleted_milestone"),
  1629  			}
  1630  			if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
  1631  				comment.OldMilestone = ghostMilestone
  1632  			}
  1633  			if comment.MilestoneID > 0 && comment.Milestone == nil {
  1634  				comment.Milestone = ghostMilestone
  1635  			}
  1636  		} else if comment.Type == issues_model.CommentTypeProject {
  1637  
  1638  			if err = comment.LoadProject(ctx); err != nil {
  1639  				ctx.ServerError("LoadProject", err)
  1640  				return
  1641  			}
  1642  
  1643  			ghostProject := &project_model.Project{
  1644  				ID:    -1,
  1645  				Title: ctx.Tr("repo.issues.deleted_project"),
  1646  			}
  1647  
  1648  			if comment.OldProjectID > 0 && comment.OldProject == nil {
  1649  				comment.OldProject = ghostProject
  1650  			}
  1651  
  1652  			if comment.ProjectID > 0 && comment.Project == nil {
  1653  				comment.Project = ghostProject
  1654  			}
  1655  
  1656  		} else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
  1657  			if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil {
  1658  				ctx.ServerError("LoadAssigneeUserAndTeam", err)
  1659  				return
  1660  			}
  1661  		} else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency {
  1662  			if err = comment.LoadDepIssueDetails(ctx); err != nil {
  1663  				if !issues_model.IsErrIssueNotExist(err) {
  1664  					ctx.ServerError("LoadDepIssueDetails", err)
  1665  					return
  1666  				}
  1667  			}
  1668  		} else if comment.Type.HasContentSupport() {
  1669  			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1670  				Links: markup.Links{
  1671  					Base: ctx.Repo.RepoLink,
  1672  				},
  1673  				Metas:   ctx.Repo.Repository.ComposeMetas(),
  1674  				GitRepo: ctx.Repo.GitRepo,
  1675  				Ctx:     ctx,
  1676  			}, comment.Content)
  1677  			if err != nil {
  1678  				ctx.ServerError("RenderString", err)
  1679  				return
  1680  			}
  1681  			if err = comment.LoadReview(ctx); err != nil && !issues_model.IsErrReviewNotExist(err) {
  1682  				ctx.ServerError("LoadReview", err)
  1683  				return
  1684  			}
  1685  			participants = addParticipant(comment.Poster, participants)
  1686  			if comment.Review == nil {
  1687  				continue
  1688  			}
  1689  			if err = comment.Review.LoadAttributes(ctx); err != nil {
  1690  				if !user_model.IsErrUserNotExist(err) {
  1691  					ctx.ServerError("Review.LoadAttributes", err)
  1692  					return
  1693  				}
  1694  				comment.Review.Reviewer = user_model.NewGhostUser()
  1695  			}
  1696  			if err = comment.Review.LoadCodeComments(ctx); err != nil {
  1697  				ctx.ServerError("Review.LoadCodeComments", err)
  1698  				return
  1699  			}
  1700  			for _, codeComments := range comment.Review.CodeComments {
  1701  				for _, lineComments := range codeComments {
  1702  					for _, c := range lineComments {
  1703  						// Check tag.
  1704  						role, ok = marked[c.PosterID]
  1705  						if ok {
  1706  							c.ShowRole = role
  1707  							continue
  1708  						}
  1709  
  1710  						c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue, c.HasOriginalAuthor())
  1711  						if err != nil {
  1712  							ctx.ServerError("roleDescriptor", err)
  1713  							return
  1714  						}
  1715  						marked[c.PosterID] = c.ShowRole
  1716  						participants = addParticipant(c.Poster, participants)
  1717  					}
  1718  				}
  1719  			}
  1720  			if err = comment.LoadResolveDoer(ctx); err != nil {
  1721  				ctx.ServerError("LoadResolveDoer", err)
  1722  				return
  1723  			}
  1724  		} else if comment.Type == issues_model.CommentTypePullRequestPush {
  1725  			participants = addParticipant(comment.Poster, participants)
  1726  			if err = comment.LoadPushCommits(ctx); err != nil {
  1727  				ctx.ServerError("LoadPushCommits", err)
  1728  				return
  1729  			}
  1730  		} else if comment.Type == issues_model.CommentTypeAddTimeManual ||
  1731  			comment.Type == issues_model.CommentTypeStopTracking ||
  1732  			comment.Type == issues_model.CommentTypeDeleteTimeManual {
  1733  			// drop error since times could be pruned from DB..
  1734  			_ = comment.LoadTime()
  1735  			if comment.Content != "" {
  1736  				// Content before v1.21 did store the formated string instead of seconds,
  1737  				// so "|" is used as delimeter to mark the new format
  1738  				if comment.Content[0] != '|' {
  1739  					// handle old time comments that have formatted text stored
  1740  					comment.RenderedContent = comment.Content
  1741  					comment.Content = ""
  1742  				} else {
  1743  					// else it's just a duration in seconds to pass on to the frontend
  1744  					comment.Content = comment.Content[1:]
  1745  				}
  1746  			}
  1747  		}
  1748  
  1749  		if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull {
  1750  			// record ID of the latest closed/merged comment.
  1751  			// if PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered.
  1752  			latestCloseCommentID = comment.ID
  1753  		}
  1754  	}
  1755  
  1756  	ctx.Data["LatestCloseCommentID"] = latestCloseCommentID
  1757  
  1758  	// Combine multiple label assignments into a single comment
  1759  	combineLabelComments(issue)
  1760  
  1761  	getBranchData(ctx, issue)
  1762  	if issue.IsPull {
  1763  		pull := issue.PullRequest
  1764  		pull.Issue = issue
  1765  		canDelete := false
  1766  		ctx.Data["AllowMerge"] = false
  1767  
  1768  		if ctx.IsSigned {
  1769  			if err := pull.LoadHeadRepo(ctx); err != nil {
  1770  				log.Error("LoadHeadRepo: %v", err)
  1771  			} else if pull.HeadRepo != nil {
  1772  				perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
  1773  				if err != nil {
  1774  					ctx.ServerError("GetUserRepoPermission", err)
  1775  					return
  1776  				}
  1777  				if perm.CanWrite(unit.TypeCode) {
  1778  					// Check if branch is not protected
  1779  					if pull.HeadBranch != pull.HeadRepo.DefaultBranch {
  1780  						if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil {
  1781  							log.Error("IsProtectedBranch: %v", err)
  1782  						} else if !protected {
  1783  							canDelete = true
  1784  							ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
  1785  						}
  1786  					}
  1787  					ctx.Data["CanWriteToHeadRepo"] = true
  1788  				}
  1789  			}
  1790  
  1791  			if err := pull.LoadBaseRepo(ctx); err != nil {
  1792  				log.Error("LoadBaseRepo: %v", err)
  1793  			}
  1794  			perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
  1795  			if err != nil {
  1796  				ctx.ServerError("GetUserRepoPermission", err)
  1797  				return
  1798  			}
  1799  			ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
  1800  			if err != nil {
  1801  				ctx.ServerError("IsUserAllowedToMerge", err)
  1802  				return
  1803  			}
  1804  
  1805  			if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
  1806  				ctx.ServerError("CanMarkConversation", err)
  1807  				return
  1808  			}
  1809  		}
  1810  
  1811  		prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
  1812  		if err != nil {
  1813  			ctx.ServerError("GetUnit", err)
  1814  			return
  1815  		}
  1816  		prConfig := prUnit.PullRequestsConfig()
  1817  
  1818  		var mergeStyle repo_model.MergeStyle
  1819  		// Check correct values and select default
  1820  		if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
  1821  			!prConfig.IsMergeStyleAllowed(ms) {
  1822  			defaultMergeStyle := prConfig.GetDefaultMergeStyle()
  1823  			if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
  1824  				mergeStyle = defaultMergeStyle
  1825  			} else if prConfig.AllowMerge {
  1826  				mergeStyle = repo_model.MergeStyleMerge
  1827  			} else if prConfig.AllowRebase {
  1828  				mergeStyle = repo_model.MergeStyleRebase
  1829  			} else if prConfig.AllowRebaseMerge {
  1830  				mergeStyle = repo_model.MergeStyleRebaseMerge
  1831  			} else if prConfig.AllowSquash {
  1832  				mergeStyle = repo_model.MergeStyleSquash
  1833  			} else if prConfig.AllowManualMerge {
  1834  				mergeStyle = repo_model.MergeStyleManuallyMerged
  1835  			}
  1836  		}
  1837  
  1838  		ctx.Data["MergeStyle"] = mergeStyle
  1839  
  1840  		defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
  1841  		if err != nil {
  1842  			ctx.ServerError("GetDefaultMergeMessage", err)
  1843  			return
  1844  		}
  1845  		ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
  1846  		ctx.Data["DefaultMergeBody"] = defaultMergeBody
  1847  
  1848  		defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
  1849  		if err != nil {
  1850  			ctx.ServerError("GetDefaultSquashMergeMessage", err)
  1851  			return
  1852  		}
  1853  		ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
  1854  		ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
  1855  
  1856  		pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
  1857  		if err != nil {
  1858  			ctx.ServerError("LoadProtectedBranch", err)
  1859  			return
  1860  		}
  1861  		ctx.Data["ShowMergeInstructions"] = true
  1862  		if pb != nil {
  1863  			pb.Repo = pull.BaseRepo
  1864  			var showMergeInstructions bool
  1865  			if ctx.Doer != nil {
  1866  				showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer)
  1867  			}
  1868  			ctx.Data["ProtectedBranch"] = pb
  1869  			ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
  1870  			ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
  1871  			ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
  1872  			ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
  1873  			ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
  1874  			ctx.Data["RequireSigned"] = pb.RequireSignedCommits
  1875  			ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
  1876  			ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
  1877  			ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
  1878  			ctx.Data["ShowMergeInstructions"] = showMergeInstructions
  1879  		}
  1880  		ctx.Data["WillSign"] = false
  1881  		if ctx.Doer != nil {
  1882  			sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
  1883  			ctx.Data["WillSign"] = sign
  1884  			ctx.Data["SigningKey"] = key
  1885  			if err != nil {
  1886  				if asymkey_service.IsErrWontSign(err) {
  1887  					ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason
  1888  				} else {
  1889  					ctx.Data["WontSignReason"] = "error"
  1890  					log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
  1891  				}
  1892  			}
  1893  		} else {
  1894  			ctx.Data["WontSignReason"] = "not_signed_in"
  1895  		}
  1896  
  1897  		isPullBranchDeletable := canDelete &&
  1898  			pull.HeadRepo != nil &&
  1899  			git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
  1900  			(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
  1901  
  1902  		if isPullBranchDeletable && pull.HasMerged {
  1903  			exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
  1904  			if err != nil {
  1905  				ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
  1906  				return
  1907  			}
  1908  
  1909  			isPullBranchDeletable = !exist
  1910  		}
  1911  		ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable
  1912  
  1913  		stillCanManualMerge := func() bool {
  1914  			if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
  1915  				return false
  1916  			}
  1917  			if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
  1918  				return false
  1919  			}
  1920  			if (ctx.Doer.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
  1921  				return true
  1922  			}
  1923  
  1924  			return false
  1925  		}
  1926  
  1927  		ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
  1928  
  1929  		// Check if there is a pending pr merge
  1930  		ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
  1931  		if err != nil {
  1932  			ctx.ServerError("GetScheduledMergeByPullID", err)
  1933  			return
  1934  		}
  1935  	}
  1936  
  1937  	// Get Dependencies
  1938  	blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
  1939  	if err != nil {
  1940  		ctx.ServerError("BlockedByDependencies", err)
  1941  		return
  1942  	}
  1943  	ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy)
  1944  	if ctx.Written() {
  1945  		return
  1946  	}
  1947  
  1948  	blocking, err := issue.BlockingDependencies(ctx)
  1949  	if err != nil {
  1950  		ctx.ServerError("BlockingDependencies", err)
  1951  		return
  1952  	}
  1953  
  1954  	ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
  1955  	if ctx.Written() {
  1956  		return
  1957  	}
  1958  
  1959  	var pinAllowed bool
  1960  	if !issue.IsPinned() {
  1961  		pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull)
  1962  		if err != nil {
  1963  			ctx.ServerError("IsNewPinAllowed", err)
  1964  			return
  1965  		}
  1966  	} else {
  1967  		pinAllowed = true
  1968  	}
  1969  
  1970  	ctx.Data["Participants"] = participants
  1971  	ctx.Data["NumParticipants"] = len(participants)
  1972  	ctx.Data["Issue"] = issue
  1973  	ctx.Data["Reference"] = issue.Ref
  1974  	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
  1975  	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
  1976  	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  1977  	ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
  1978  	ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
  1979  	ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
  1980  	ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
  1981  	ctx.Data["NewPinAllowed"] = pinAllowed
  1982  	ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
  1983  
  1984  	var hiddenCommentTypes *big.Int
  1985  	if ctx.IsSigned {
  1986  		val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
  1987  		if err != nil {
  1988  			ctx.ServerError("GetUserSetting", err)
  1989  			return
  1990  		}
  1991  		hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
  1992  	}
  1993  	ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
  1994  		return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
  1995  	}
  1996  	// For sidebar
  1997  	PrepareBranchList(ctx)
  1998  
  1999  	if ctx.Written() {
  2000  		return
  2001  	}
  2002  
  2003  	tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
  2004  	if err != nil {
  2005  		ctx.ServerError("GetTagNamesByRepoID", err)
  2006  		return
  2007  	}
  2008  	ctx.Data["Tags"] = tags
  2009  
  2010  	ctx.HTML(http.StatusOK, tplIssueView)
  2011  }
  2012  
  2013  // checkBlockedByIssues return canRead and notPermitted
  2014  func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
  2015  	repoPerms := make(map[int64]access_model.Permission)
  2016  	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
  2017  	for _, blocker := range blockers {
  2018  		// Get the permissions for this repository
  2019  		// If the repo ID exists in the map, return the exist permissions
  2020  		// else get the permission and add it to the map
  2021  		var perm access_model.Permission
  2022  		existPerm, ok := repoPerms[blocker.RepoID]
  2023  		if ok {
  2024  			perm = existPerm
  2025  		} else {
  2026  			var err error
  2027  			perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
  2028  			if err != nil {
  2029  				ctx.ServerError("GetUserRepoPermission", err)
  2030  				return nil, nil
  2031  			}
  2032  			repoPerms[blocker.RepoID] = perm
  2033  		}
  2034  		if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
  2035  			canRead = append(canRead, blocker)
  2036  		} else {
  2037  			notPermitted = append(notPermitted, blocker)
  2038  		}
  2039  	}
  2040  	sortDependencyInfo(canRead)
  2041  	sortDependencyInfo(notPermitted)
  2042  	return canRead, notPermitted
  2043  }
  2044  
  2045  func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
  2046  	sort.Slice(blockers, func(i, j int) bool {
  2047  		if blockers[i].RepoID == blockers[j].RepoID {
  2048  			return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix
  2049  		}
  2050  		return blockers[i].RepoID < blockers[j].RepoID
  2051  	})
  2052  }
  2053  
  2054  // GetActionIssue will return the issue which is used in the context.
  2055  func GetActionIssue(ctx *context.Context) *issues_model.Issue {
  2056  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  2057  	if err != nil {
  2058  		ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
  2059  		return nil
  2060  	}
  2061  	issue.Repo = ctx.Repo.Repository
  2062  	checkIssueRights(ctx, issue)
  2063  	if ctx.Written() {
  2064  		return nil
  2065  	}
  2066  	if err = issue.LoadAttributes(ctx); err != nil {
  2067  		ctx.ServerError("LoadAttributes", err)
  2068  		return nil
  2069  	}
  2070  	return issue
  2071  }
  2072  
  2073  func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
  2074  	if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
  2075  		!issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
  2076  		ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  2077  	}
  2078  }
  2079  
  2080  func getActionIssues(ctx *context.Context) issues_model.IssueList {
  2081  	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
  2082  	if len(commaSeparatedIssueIDs) == 0 {
  2083  		return nil
  2084  	}
  2085  	issueIDs := make([]int64, 0, 10)
  2086  	for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
  2087  		issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
  2088  		if err != nil {
  2089  			ctx.ServerError("ParseInt", err)
  2090  			return nil
  2091  		}
  2092  		issueIDs = append(issueIDs, issueID)
  2093  	}
  2094  	issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
  2095  	if err != nil {
  2096  		ctx.ServerError("GetIssuesByIDs", err)
  2097  		return nil
  2098  	}
  2099  	// Check access rights for all issues
  2100  	issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
  2101  	prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
  2102  	for _, issue := range issues {
  2103  		if issue.RepoID != ctx.Repo.Repository.ID {
  2104  			ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
  2105  			return nil
  2106  		}
  2107  		if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
  2108  			ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  2109  			return nil
  2110  		}
  2111  		if err = issue.LoadAttributes(ctx); err != nil {
  2112  			ctx.ServerError("LoadAttributes", err)
  2113  			return nil
  2114  		}
  2115  	}
  2116  	return issues
  2117  }
  2118  
  2119  // GetIssueInfo get an issue of a repository
  2120  func GetIssueInfo(ctx *context.Context) {
  2121  	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  2122  	if err != nil {
  2123  		if issues_model.IsErrIssueNotExist(err) {
  2124  			ctx.Error(http.StatusNotFound)
  2125  		} else {
  2126  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
  2127  		}
  2128  		return
  2129  	}
  2130  
  2131  	if issue.IsPull {
  2132  		// Need to check if Pulls are enabled and we can read Pulls
  2133  		if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
  2134  			ctx.Error(http.StatusNotFound)
  2135  			return
  2136  		}
  2137  	} else {
  2138  		// Need to check if Issues are enabled and we can read Issues
  2139  		if !ctx.Repo.CanRead(unit.TypeIssues) {
  2140  			ctx.Error(http.StatusNotFound)
  2141  			return
  2142  		}
  2143  	}
  2144  
  2145  	ctx.JSON(http.StatusOK, convert.ToIssue(ctx, issue))
  2146  }
  2147  
  2148  // UpdateIssueTitle change issue's title
  2149  func UpdateIssueTitle(ctx *context.Context) {
  2150  	issue := GetActionIssue(ctx)
  2151  	if ctx.Written() {
  2152  		return
  2153  	}
  2154  
  2155  	if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  2156  		ctx.Error(http.StatusForbidden)
  2157  		return
  2158  	}
  2159  
  2160  	title := ctx.FormTrim("title")
  2161  	if len(title) == 0 {
  2162  		ctx.Error(http.StatusNoContent)
  2163  		return
  2164  	}
  2165  
  2166  	if err := issue_service.ChangeTitle(ctx, issue, ctx.Doer, title); err != nil {
  2167  		ctx.ServerError("ChangeTitle", err)
  2168  		return
  2169  	}
  2170  
  2171  	ctx.JSON(http.StatusOK, map[string]any{
  2172  		"title": issue.Title,
  2173  	})
  2174  }
  2175  
  2176  // UpdateIssueRef change issue's ref (branch)
  2177  func UpdateIssueRef(ctx *context.Context) {
  2178  	issue := GetActionIssue(ctx)
  2179  	if ctx.Written() {
  2180  		return
  2181  	}
  2182  
  2183  	if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
  2184  		ctx.Error(http.StatusForbidden)
  2185  		return
  2186  	}
  2187  
  2188  	ref := ctx.FormTrim("ref")
  2189  
  2190  	if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil {
  2191  		ctx.ServerError("ChangeRef", err)
  2192  		return
  2193  	}
  2194  
  2195  	ctx.JSON(http.StatusOK, map[string]any{
  2196  		"ref": ref,
  2197  	})
  2198  }
  2199  
  2200  // UpdateIssueContent change issue's content
  2201  func UpdateIssueContent(ctx *context.Context) {
  2202  	issue := GetActionIssue(ctx)
  2203  	if ctx.Written() {
  2204  		return
  2205  	}
  2206  
  2207  	if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  2208  		ctx.Error(http.StatusForbidden)
  2209  		return
  2210  	}
  2211  
  2212  	if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
  2213  		ctx.ServerError("ChangeContent", err)
  2214  		return
  2215  	}
  2216  
  2217  	// when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
  2218  	if !ctx.FormBool("ignore_attachments") {
  2219  		if err := updateAttachments(ctx, issue, ctx.FormStrings("files[]")); err != nil {
  2220  			ctx.ServerError("UpdateAttachments", err)
  2221  			return
  2222  		}
  2223  	}
  2224  
  2225  	content, err := markdown.RenderString(&markup.RenderContext{
  2226  		Links: markup.Links{
  2227  			Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
  2228  		},
  2229  		Metas:   ctx.Repo.Repository.ComposeMetas(),
  2230  		GitRepo: ctx.Repo.GitRepo,
  2231  		Ctx:     ctx,
  2232  	}, issue.Content)
  2233  	if err != nil {
  2234  		ctx.ServerError("RenderString", err)
  2235  		return
  2236  	}
  2237  
  2238  	ctx.JSON(http.StatusOK, map[string]any{
  2239  		"content":     content,
  2240  		"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
  2241  	})
  2242  }
  2243  
  2244  // UpdateIssueDeadline updates an issue deadline
  2245  func UpdateIssueDeadline(ctx *context.Context) {
  2246  	form := web.GetForm(ctx).(*api.EditDeadlineOption)
  2247  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  2248  	if err != nil {
  2249  		if issues_model.IsErrIssueNotExist(err) {
  2250  			ctx.NotFound("GetIssueByIndex", err)
  2251  		} else {
  2252  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
  2253  		}
  2254  		return
  2255  	}
  2256  
  2257  	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  2258  		ctx.Error(http.StatusForbidden, "", "Not repo writer")
  2259  		return
  2260  	}
  2261  
  2262  	var deadlineUnix timeutil.TimeStamp
  2263  	var deadline time.Time
  2264  	if form.Deadline != nil && !form.Deadline.IsZero() {
  2265  		deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  2266  			23, 59, 59, 0, time.Local)
  2267  		deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  2268  	}
  2269  
  2270  	if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
  2271  		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
  2272  		return
  2273  	}
  2274  
  2275  	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  2276  }
  2277  
  2278  // UpdateIssueMilestone change issue's milestone
  2279  func UpdateIssueMilestone(ctx *context.Context) {
  2280  	issues := getActionIssues(ctx)
  2281  	if ctx.Written() {
  2282  		return
  2283  	}
  2284  
  2285  	milestoneID := ctx.FormInt64("id")
  2286  	for _, issue := range issues {
  2287  		oldMilestoneID := issue.MilestoneID
  2288  		if oldMilestoneID == milestoneID {
  2289  			continue
  2290  		}
  2291  		issue.MilestoneID = milestoneID
  2292  		if err := issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
  2293  			ctx.ServerError("ChangeMilestoneAssign", err)
  2294  			return
  2295  		}
  2296  	}
  2297  
  2298  	ctx.JSONOK()
  2299  }
  2300  
  2301  // UpdateIssueAssignee change issue's or pull's assignee
  2302  func UpdateIssueAssignee(ctx *context.Context) {
  2303  	issues := getActionIssues(ctx)
  2304  	if ctx.Written() {
  2305  		return
  2306  	}
  2307  
  2308  	assigneeID := ctx.FormInt64("id")
  2309  	action := ctx.FormString("action")
  2310  
  2311  	for _, issue := range issues {
  2312  		switch action {
  2313  		case "clear":
  2314  			if err := issue_service.DeleteNotPassedAssignee(ctx, issue, ctx.Doer, []*user_model.User{}); err != nil {
  2315  				ctx.ServerError("ClearAssignees", err)
  2316  				return
  2317  			}
  2318  		default:
  2319  			assignee, err := user_model.GetUserByID(ctx, assigneeID)
  2320  			if err != nil {
  2321  				ctx.ServerError("GetUserByID", err)
  2322  				return
  2323  			}
  2324  
  2325  			valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
  2326  			if err != nil {
  2327  				ctx.ServerError("canBeAssigned", err)
  2328  				return
  2329  			}
  2330  			if !valid {
  2331  				ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
  2332  				return
  2333  			}
  2334  
  2335  			_, _, err = issue_service.ToggleAssigneeWithNotify(ctx, issue, ctx.Doer, assigneeID)
  2336  			if err != nil {
  2337  				ctx.ServerError("ToggleAssignee", err)
  2338  				return
  2339  			}
  2340  		}
  2341  	}
  2342  	ctx.JSONOK()
  2343  }
  2344  
  2345  // UpdatePullReviewRequest add or remove review request
  2346  func UpdatePullReviewRequest(ctx *context.Context) {
  2347  	issues := getActionIssues(ctx)
  2348  	if ctx.Written() {
  2349  		return
  2350  	}
  2351  
  2352  	reviewID := ctx.FormInt64("id")
  2353  	action := ctx.FormString("action")
  2354  
  2355  	// TODO: Not support 'clear' now
  2356  	if action != "attach" && action != "detach" {
  2357  		ctx.Status(http.StatusForbidden)
  2358  		return
  2359  	}
  2360  
  2361  	for _, issue := range issues {
  2362  		if err := issue.LoadRepo(ctx); err != nil {
  2363  			ctx.ServerError("issue.LoadRepo", err)
  2364  			return
  2365  		}
  2366  
  2367  		if !issue.IsPull {
  2368  			log.Warn(
  2369  				"UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
  2370  				issue.Repo, issue.Index,
  2371  			)
  2372  			ctx.Status(http.StatusForbidden)
  2373  			return
  2374  		}
  2375  		if reviewID < 0 {
  2376  			// negative reviewIDs represent team requests
  2377  			if err := issue.Repo.LoadOwner(ctx); err != nil {
  2378  				ctx.ServerError("issue.Repo.LoadOwner", err)
  2379  				return
  2380  			}
  2381  
  2382  			if !issue.Repo.Owner.IsOrganization() {
  2383  				log.Warn(
  2384  					"UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
  2385  					issue.Repo.FullName(), issue.Index, issue.Repo.ID,
  2386  				)
  2387  				ctx.Status(http.StatusForbidden)
  2388  				return
  2389  			}
  2390  
  2391  			team, err := organization.GetTeamByID(ctx, -reviewID)
  2392  			if err != nil {
  2393  				ctx.ServerError("GetTeamByID", err)
  2394  				return
  2395  			}
  2396  
  2397  			if team.OrgID != issue.Repo.OwnerID {
  2398  				log.Warn(
  2399  					"UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
  2400  					team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
  2401  				ctx.Status(http.StatusForbidden)
  2402  				return
  2403  			}
  2404  
  2405  			err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue)
  2406  			if err != nil {
  2407  				if issues_model.IsErrNotValidReviewRequest(err) {
  2408  					log.Warn(
  2409  						"UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
  2410  						team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
  2411  						err,
  2412  					)
  2413  					ctx.Status(http.StatusForbidden)
  2414  					return
  2415  				}
  2416  				ctx.ServerError("IsValidTeamReviewRequest", err)
  2417  				return
  2418  			}
  2419  
  2420  			_, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach")
  2421  			if err != nil {
  2422  				ctx.ServerError("TeamReviewRequest", err)
  2423  				return
  2424  			}
  2425  			continue
  2426  		}
  2427  
  2428  		reviewer, err := user_model.GetUserByID(ctx, reviewID)
  2429  		if err != nil {
  2430  			if user_model.IsErrUserNotExist(err) {
  2431  				log.Warn(
  2432  					"UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
  2433  					reviewID, issue.Repo, issue.Index,
  2434  					err,
  2435  				)
  2436  				ctx.Status(http.StatusForbidden)
  2437  				return
  2438  			}
  2439  			ctx.ServerError("GetUserByID", err)
  2440  			return
  2441  		}
  2442  
  2443  		err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil)
  2444  		if err != nil {
  2445  			if issues_model.IsErrNotValidReviewRequest(err) {
  2446  				log.Warn(
  2447  					"UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
  2448  					reviewer, issue.Repo, issue.Index,
  2449  					err,
  2450  				)
  2451  				ctx.Status(http.StatusForbidden)
  2452  				return
  2453  			}
  2454  			ctx.ServerError("isValidReviewRequest", err)
  2455  			return
  2456  		}
  2457  
  2458  		_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
  2459  		if err != nil {
  2460  			ctx.ServerError("ReviewRequest", err)
  2461  			return
  2462  		}
  2463  	}
  2464  
  2465  	ctx.JSONOK()
  2466  }
  2467  
  2468  // SearchIssues searches for issues across the repositories that the user has access to
  2469  func SearchIssues(ctx *context.Context) {
  2470  	before, since, err := context.GetQueryBeforeSince(ctx.Base)
  2471  	if err != nil {
  2472  		ctx.Error(http.StatusUnprocessableEntity, err.Error())
  2473  		return
  2474  	}
  2475  
  2476  	var isClosed util.OptionalBool
  2477  	switch ctx.FormString("state") {
  2478  	case "closed":
  2479  		isClosed = util.OptionalBoolTrue
  2480  	case "all":
  2481  		isClosed = util.OptionalBoolNone
  2482  	default:
  2483  		isClosed = util.OptionalBoolFalse
  2484  	}
  2485  
  2486  	var (
  2487  		repoIDs   []int64
  2488  		allPublic bool
  2489  	)
  2490  	{
  2491  		// find repos user can access (for issue search)
  2492  		opts := &repo_model.SearchRepoOptions{
  2493  			Private:     false,
  2494  			AllPublic:   true,
  2495  			TopicOnly:   false,
  2496  			Collaborate: util.OptionalBoolNone,
  2497  			// This needs to be a column that is not nil in fixtures or
  2498  			// MySQL will return different results when sorting by null in some cases
  2499  			OrderBy: db.SearchOrderByAlphabetically,
  2500  			Actor:   ctx.Doer,
  2501  		}
  2502  		if ctx.IsSigned {
  2503  			opts.Private = true
  2504  			opts.AllLimited = true
  2505  		}
  2506  		if ctx.FormString("owner") != "" {
  2507  			owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
  2508  			if err != nil {
  2509  				if user_model.IsErrUserNotExist(err) {
  2510  					ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
  2511  				} else {
  2512  					ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
  2513  				}
  2514  				return
  2515  			}
  2516  			opts.OwnerID = owner.ID
  2517  			opts.AllLimited = false
  2518  			opts.AllPublic = false
  2519  			opts.Collaborate = util.OptionalBoolFalse
  2520  		}
  2521  		if ctx.FormString("team") != "" {
  2522  			if ctx.FormString("owner") == "" {
  2523  				ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
  2524  				return
  2525  			}
  2526  			team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
  2527  			if err != nil {
  2528  				if organization.IsErrTeamNotExist(err) {
  2529  					ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
  2530  				} else {
  2531  					ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
  2532  				}
  2533  				return
  2534  			}
  2535  			opts.TeamID = team.ID
  2536  		}
  2537  
  2538  		if opts.AllPublic {
  2539  			allPublic = true
  2540  			opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
  2541  		}
  2542  		repoIDs, _, err = repo_model.SearchRepositoryIDs(opts)
  2543  		if err != nil {
  2544  			ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
  2545  			return
  2546  		}
  2547  		if len(repoIDs) == 0 {
  2548  			// no repos found, don't let the indexer return all repos
  2549  			repoIDs = []int64{0}
  2550  		}
  2551  	}
  2552  
  2553  	keyword := ctx.FormTrim("q")
  2554  	if strings.IndexByte(keyword, 0) >= 0 {
  2555  		keyword = ""
  2556  	}
  2557  
  2558  	var isPull util.OptionalBool
  2559  	switch ctx.FormString("type") {
  2560  	case "pulls":
  2561  		isPull = util.OptionalBoolTrue
  2562  	case "issues":
  2563  		isPull = util.OptionalBoolFalse
  2564  	default:
  2565  		isPull = util.OptionalBoolNone
  2566  	}
  2567  
  2568  	var includedAnyLabels []int64
  2569  	{
  2570  
  2571  		labels := ctx.FormTrim("labels")
  2572  		var includedLabelNames []string
  2573  		if len(labels) > 0 {
  2574  			includedLabelNames = strings.Split(labels, ",")
  2575  		}
  2576  		includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
  2577  		if err != nil {
  2578  			ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
  2579  			return
  2580  		}
  2581  	}
  2582  
  2583  	var includedMilestones []int64
  2584  	{
  2585  		milestones := ctx.FormTrim("milestones")
  2586  		var includedMilestoneNames []string
  2587  		if len(milestones) > 0 {
  2588  			includedMilestoneNames = strings.Split(milestones, ",")
  2589  		}
  2590  		includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
  2591  		if err != nil {
  2592  			ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
  2593  			return
  2594  		}
  2595  	}
  2596  
  2597  	var projectID *int64
  2598  	if v := ctx.FormInt64("project"); v > 0 {
  2599  		projectID = &v
  2600  	}
  2601  
  2602  	// this api is also used in UI,
  2603  	// so the default limit is set to fit UI needs
  2604  	limit := ctx.FormInt("limit")
  2605  	if limit == 0 {
  2606  		limit = setting.UI.IssuePagingNum
  2607  	} else if limit > setting.API.MaxResponseItems {
  2608  		limit = setting.API.MaxResponseItems
  2609  	}
  2610  
  2611  	searchOpt := &issue_indexer.SearchOptions{
  2612  		Paginator: &db.ListOptions{
  2613  			Page:     ctx.FormInt("page"),
  2614  			PageSize: limit,
  2615  		},
  2616  		Keyword:             keyword,
  2617  		RepoIDs:             repoIDs,
  2618  		AllPublic:           allPublic,
  2619  		IsPull:              isPull,
  2620  		IsClosed:            isClosed,
  2621  		IncludedAnyLabelIDs: includedAnyLabels,
  2622  		MilestoneIDs:        includedMilestones,
  2623  		ProjectID:           projectID,
  2624  		SortBy:              issue_indexer.SortByCreatedDesc,
  2625  	}
  2626  
  2627  	if since != 0 {
  2628  		searchOpt.UpdatedAfterUnix = &since
  2629  	}
  2630  	if before != 0 {
  2631  		searchOpt.UpdatedBeforeUnix = &before
  2632  	}
  2633  
  2634  	if ctx.IsSigned {
  2635  		ctxUserID := ctx.Doer.ID
  2636  		if ctx.FormBool("created") {
  2637  			searchOpt.PosterID = &ctxUserID
  2638  		}
  2639  		if ctx.FormBool("assigned") {
  2640  			searchOpt.AssigneeID = &ctxUserID
  2641  		}
  2642  		if ctx.FormBool("mentioned") {
  2643  			searchOpt.MentionID = &ctxUserID
  2644  		}
  2645  		if ctx.FormBool("review_requested") {
  2646  			searchOpt.ReviewRequestedID = &ctxUserID
  2647  		}
  2648  		if ctx.FormBool("reviewed") {
  2649  			searchOpt.ReviewedID = &ctxUserID
  2650  		}
  2651  	}
  2652  
  2653  	// FIXME: It's unsupported to sort by priority repo when searching by indexer,
  2654  	//        it's indeed an regression, but I think it is worth to support filtering by indexer first.
  2655  	_ = ctx.FormInt64("priority_repo_id")
  2656  
  2657  	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
  2658  	if err != nil {
  2659  		ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
  2660  		return
  2661  	}
  2662  	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
  2663  	if err != nil {
  2664  		ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
  2665  		return
  2666  	}
  2667  
  2668  	ctx.SetTotalCountHeader(total)
  2669  	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
  2670  }
  2671  
  2672  func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
  2673  	userName := ctx.FormString(queryName)
  2674  	if len(userName) == 0 {
  2675  		return 0
  2676  	}
  2677  
  2678  	user, err := user_model.GetUserByName(ctx, userName)
  2679  	if user_model.IsErrUserNotExist(err) {
  2680  		ctx.NotFound("", err)
  2681  		return 0
  2682  	}
  2683  
  2684  	if err != nil {
  2685  		ctx.Error(http.StatusInternalServerError, err.Error())
  2686  		return 0
  2687  	}
  2688  
  2689  	return user.ID
  2690  }
  2691  
  2692  // ListIssues list the issues of a repository
  2693  func ListIssues(ctx *context.Context) {
  2694  	before, since, err := context.GetQueryBeforeSince(ctx.Base)
  2695  	if err != nil {
  2696  		ctx.Error(http.StatusUnprocessableEntity, err.Error())
  2697  		return
  2698  	}
  2699  
  2700  	var isClosed util.OptionalBool
  2701  	switch ctx.FormString("state") {
  2702  	case "closed":
  2703  		isClosed = util.OptionalBoolTrue
  2704  	case "all":
  2705  		isClosed = util.OptionalBoolNone
  2706  	default:
  2707  		isClosed = util.OptionalBoolFalse
  2708  	}
  2709  
  2710  	keyword := ctx.FormTrim("q")
  2711  	if strings.IndexByte(keyword, 0) >= 0 {
  2712  		keyword = ""
  2713  	}
  2714  
  2715  	var labelIDs []int64
  2716  	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
  2717  		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
  2718  		if err != nil {
  2719  			ctx.Error(http.StatusInternalServerError, err.Error())
  2720  			return
  2721  		}
  2722  	}
  2723  
  2724  	var mileIDs []int64
  2725  	if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
  2726  		for i := range part {
  2727  			// uses names and fall back to ids
  2728  			// non existent milestones are discarded
  2729  			mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
  2730  			if err == nil {
  2731  				mileIDs = append(mileIDs, mile.ID)
  2732  				continue
  2733  			}
  2734  			if !issues_model.IsErrMilestoneNotExist(err) {
  2735  				ctx.Error(http.StatusInternalServerError, err.Error())
  2736  				return
  2737  			}
  2738  			id, err := strconv.ParseInt(part[i], 10, 64)
  2739  			if err != nil {
  2740  				continue
  2741  			}
  2742  			mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
  2743  			if err == nil {
  2744  				mileIDs = append(mileIDs, mile.ID)
  2745  				continue
  2746  			}
  2747  			if issues_model.IsErrMilestoneNotExist(err) {
  2748  				continue
  2749  			}
  2750  			ctx.Error(http.StatusInternalServerError, err.Error())
  2751  		}
  2752  	}
  2753  
  2754  	var projectID *int64
  2755  	if v := ctx.FormInt64("project"); v > 0 {
  2756  		projectID = &v
  2757  	}
  2758  
  2759  	var isPull util.OptionalBool
  2760  	switch ctx.FormString("type") {
  2761  	case "pulls":
  2762  		isPull = util.OptionalBoolTrue
  2763  	case "issues":
  2764  		isPull = util.OptionalBoolFalse
  2765  	default:
  2766  		isPull = util.OptionalBoolNone
  2767  	}
  2768  
  2769  	// FIXME: we should be more efficient here
  2770  	createdByID := getUserIDForFilter(ctx, "created_by")
  2771  	if ctx.Written() {
  2772  		return
  2773  	}
  2774  	assignedByID := getUserIDForFilter(ctx, "assigned_by")
  2775  	if ctx.Written() {
  2776  		return
  2777  	}
  2778  	mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
  2779  	if ctx.Written() {
  2780  		return
  2781  	}
  2782  
  2783  	searchOpt := &issue_indexer.SearchOptions{
  2784  		Paginator: &db.ListOptions{
  2785  			Page:     ctx.FormInt("page"),
  2786  			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
  2787  		},
  2788  		Keyword:        keyword,
  2789  		RepoIDs:        []int64{ctx.Repo.Repository.ID},
  2790  		IsPull:         isPull,
  2791  		IsClosed:       isClosed,
  2792  		ProjectBoardID: projectID,
  2793  		SortBy:         issue_indexer.SortByCreatedDesc,
  2794  	}
  2795  	if since != 0 {
  2796  		searchOpt.UpdatedAfterUnix = &since
  2797  	}
  2798  	if before != 0 {
  2799  		searchOpt.UpdatedBeforeUnix = &before
  2800  	}
  2801  	if len(labelIDs) == 1 && labelIDs[0] == 0 {
  2802  		searchOpt.NoLabelOnly = true
  2803  	} else {
  2804  		for _, labelID := range labelIDs {
  2805  			if labelID > 0 {
  2806  				searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
  2807  			} else {
  2808  				searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
  2809  			}
  2810  		}
  2811  	}
  2812  
  2813  	if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
  2814  		searchOpt.MilestoneIDs = []int64{0}
  2815  	} else {
  2816  		searchOpt.MilestoneIDs = mileIDs
  2817  	}
  2818  
  2819  	if createdByID > 0 {
  2820  		searchOpt.PosterID = &createdByID
  2821  	}
  2822  	if assignedByID > 0 {
  2823  		searchOpt.AssigneeID = &assignedByID
  2824  	}
  2825  	if mentionedByID > 0 {
  2826  		searchOpt.MentionID = &mentionedByID
  2827  	}
  2828  
  2829  	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
  2830  	if err != nil {
  2831  		ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
  2832  		return
  2833  	}
  2834  	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
  2835  	if err != nil {
  2836  		ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
  2837  		return
  2838  	}
  2839  
  2840  	ctx.SetTotalCountHeader(total)
  2841  	ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
  2842  }
  2843  
  2844  func BatchDeleteIssues(ctx *context.Context) {
  2845  	issues := getActionIssues(ctx)
  2846  	if ctx.Written() {
  2847  		return
  2848  	}
  2849  	for _, issue := range issues {
  2850  		if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
  2851  			ctx.ServerError("DeleteIssue", err)
  2852  			return
  2853  		}
  2854  	}
  2855  	ctx.JSONOK()
  2856  }
  2857  
  2858  // UpdateIssueStatus change issue's status
  2859  func UpdateIssueStatus(ctx *context.Context) {
  2860  	issues := getActionIssues(ctx)
  2861  	if ctx.Written() {
  2862  		return
  2863  	}
  2864  
  2865  	var isClosed bool
  2866  	switch action := ctx.FormString("action"); action {
  2867  	case "open":
  2868  		isClosed = false
  2869  	case "close":
  2870  		isClosed = true
  2871  	default:
  2872  		log.Warn("Unrecognized action: %s", action)
  2873  	}
  2874  
  2875  	if _, err := issues.LoadRepositories(ctx); err != nil {
  2876  		ctx.ServerError("LoadRepositories", err)
  2877  		return
  2878  	}
  2879  	if err := issues.LoadPullRequests(ctx); err != nil {
  2880  		ctx.ServerError("LoadPullRequests", err)
  2881  		return
  2882  	}
  2883  
  2884  	for _, issue := range issues {
  2885  		if issue.IsPull && issue.PullRequest.HasMerged {
  2886  			continue
  2887  		}
  2888  		if issue.IsClosed != isClosed {
  2889  			if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
  2890  				if issues_model.IsErrDependenciesLeft(err) {
  2891  					ctx.JSON(http.StatusPreconditionFailed, map[string]any{
  2892  						"error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
  2893  					})
  2894  					return
  2895  				}
  2896  				ctx.ServerError("ChangeStatus", err)
  2897  				return
  2898  			}
  2899  		}
  2900  	}
  2901  	ctx.JSONOK()
  2902  }
  2903  
  2904  // NewComment create a comment for issue
  2905  func NewComment(ctx *context.Context) {
  2906  	form := web.GetForm(ctx).(*forms.CreateCommentForm)
  2907  	issue := GetActionIssue(ctx)
  2908  	if ctx.Written() {
  2909  		return
  2910  	}
  2911  
  2912  	if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  2913  		if log.IsTrace() {
  2914  			if ctx.IsSigned {
  2915  				issueType := "issues"
  2916  				if issue.IsPull {
  2917  					issueType = "pulls"
  2918  				}
  2919  				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  2920  					"User in Repo has Permissions: %-+v",
  2921  					ctx.Doer,
  2922  					issue.PosterID,
  2923  					issueType,
  2924  					ctx.Repo.Repository,
  2925  					ctx.Repo.Permission)
  2926  			} else {
  2927  				log.Trace("Permission Denied: Not logged in")
  2928  			}
  2929  		}
  2930  
  2931  		ctx.Error(http.StatusForbidden)
  2932  		return
  2933  	}
  2934  
  2935  	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
  2936  		ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked"))
  2937  		return
  2938  	}
  2939  
  2940  	var attachments []string
  2941  	if setting.Attachment.Enabled {
  2942  		attachments = form.Files
  2943  	}
  2944  
  2945  	if ctx.HasError() {
  2946  		ctx.JSONError(ctx.GetErrMsg())
  2947  		return
  2948  	}
  2949  
  2950  	var comment *issues_model.Comment
  2951  	defer func() {
  2952  		// Check if issue admin/poster changes the status of issue.
  2953  		if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
  2954  			(form.Status == "reopen" || form.Status == "close") &&
  2955  			!(issue.IsPull && issue.PullRequest.HasMerged) {
  2956  
  2957  			// Duplication and conflict check should apply to reopen pull request.
  2958  			var pr *issues_model.PullRequest
  2959  
  2960  			if form.Status == "reopen" && issue.IsPull {
  2961  				pull := issue.PullRequest
  2962  				var err error
  2963  				pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
  2964  				if err != nil {
  2965  					if !issues_model.IsErrPullRequestNotExist(err) {
  2966  						ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
  2967  						return
  2968  					}
  2969  				}
  2970  
  2971  				// Regenerate patch and test conflict.
  2972  				if pr == nil {
  2973  					issue.PullRequest.HeadCommitID = ""
  2974  					pull_service.AddToTaskQueue(ctx, issue.PullRequest)
  2975  				}
  2976  
  2977  				// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
  2978  				// get head commit of PR
  2979  				if pull.Flow == issues_model.PullRequestFlowGithub {
  2980  					prHeadRef := pull.GetGitRefName()
  2981  					if err := pull.LoadBaseRepo(ctx); err != nil {
  2982  						ctx.ServerError("Unable to load base repo", err)
  2983  						return
  2984  					}
  2985  					prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
  2986  					if err != nil {
  2987  						ctx.ServerError("Get head commit Id of pr fail", err)
  2988  						return
  2989  					}
  2990  
  2991  					// get head commit of branch in the head repo
  2992  					if err := pull.LoadHeadRepo(ctx); err != nil {
  2993  						ctx.ServerError("Unable to load head repo", err)
  2994  						return
  2995  					}
  2996  					if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok {
  2997  						// todo localize
  2998  						ctx.JSONError("The origin branch is delete, cannot reopen.")
  2999  						return
  3000  					}
  3001  					headBranchRef := pull.GetGitHeadBranchRefName()
  3002  					headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
  3003  					if err != nil {
  3004  						ctx.ServerError("Get head commit Id of head branch fail", err)
  3005  						return
  3006  					}
  3007  
  3008  					err = pull.LoadIssue(ctx)
  3009  					if err != nil {
  3010  						ctx.ServerError("load the issue of pull request error", err)
  3011  						return
  3012  					}
  3013  
  3014  					if prHeadCommitID != headBranchCommitID {
  3015  						// force push to base repo
  3016  						err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
  3017  							Remote: pull.BaseRepo.RepoPath(),
  3018  							Branch: pull.HeadBranch + ":" + prHeadRef,
  3019  							Force:  true,
  3020  							Env:    repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
  3021  						})
  3022  						if err != nil {
  3023  							ctx.ServerError("force push error", err)
  3024  							return
  3025  						}
  3026  					}
  3027  				}
  3028  			}
  3029  
  3030  			if pr != nil {
  3031  				ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
  3032  			} else {
  3033  				isClosed := form.Status == "close"
  3034  				if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
  3035  					log.Error("ChangeStatus: %v", err)
  3036  
  3037  					if issues_model.IsErrDependenciesLeft(err) {
  3038  						if issue.IsPull {
  3039  							ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
  3040  						} else {
  3041  							ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
  3042  						}
  3043  						return
  3044  					}
  3045  				} else {
  3046  					if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
  3047  						ctx.ServerError("CreateOrStopIssueStopwatch", err)
  3048  						return
  3049  					}
  3050  
  3051  					log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
  3052  				}
  3053  			}
  3054  		}
  3055  
  3056  		// Redirect to comment hashtag if there is any actual content.
  3057  		typeName := "issues"
  3058  		if issue.IsPull {
  3059  			typeName = "pulls"
  3060  		}
  3061  		if comment != nil {
  3062  			ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
  3063  		} else {
  3064  			ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
  3065  		}
  3066  	}()
  3067  
  3068  	// Fix #321: Allow empty comments, as long as we have attachments.
  3069  	if len(form.Content) == 0 && len(attachments) == 0 {
  3070  		return
  3071  	}
  3072  
  3073  	comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
  3074  	if err != nil {
  3075  		ctx.ServerError("CreateIssueComment", err)
  3076  		return
  3077  	}
  3078  
  3079  	log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  3080  }
  3081  
  3082  // UpdateCommentContent change comment of issue's content
  3083  func UpdateCommentContent(ctx *context.Context) {
  3084  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  3085  	if err != nil {
  3086  		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  3087  		return
  3088  	}
  3089  
  3090  	if err := comment.LoadIssue(ctx); err != nil {
  3091  		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  3092  		return
  3093  	}
  3094  
  3095  	if comment.Issue.RepoID != ctx.Repo.Repository.ID {
  3096  		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
  3097  		return
  3098  	}
  3099  
  3100  	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
  3101  		ctx.Error(http.StatusForbidden)
  3102  		return
  3103  	}
  3104  
  3105  	if !comment.Type.HasContentSupport() {
  3106  		ctx.Error(http.StatusNoContent)
  3107  		return
  3108  	}
  3109  
  3110  	oldContent := comment.Content
  3111  	comment.Content = ctx.FormString("content")
  3112  	if len(comment.Content) == 0 {
  3113  		ctx.JSON(http.StatusOK, map[string]any{
  3114  			"content": "",
  3115  		})
  3116  		return
  3117  	}
  3118  	if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
  3119  		ctx.ServerError("UpdateComment", err)
  3120  		return
  3121  	}
  3122  
  3123  	if err := comment.LoadAttachments(ctx); err != nil {
  3124  		ctx.ServerError("LoadAttachments", err)
  3125  		return
  3126  	}
  3127  
  3128  	// when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
  3129  	if !ctx.FormBool("ignore_attachments") {
  3130  		if err := updateAttachments(ctx, comment, ctx.FormStrings("files[]")); err != nil {
  3131  			ctx.ServerError("UpdateAttachments", err)
  3132  			return
  3133  		}
  3134  	}
  3135  
  3136  	content, err := markdown.RenderString(&markup.RenderContext{
  3137  		Links: markup.Links{
  3138  			Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
  3139  		},
  3140  		Metas:   ctx.Repo.Repository.ComposeMetas(),
  3141  		GitRepo: ctx.Repo.GitRepo,
  3142  		Ctx:     ctx,
  3143  	}, comment.Content)
  3144  	if err != nil {
  3145  		ctx.ServerError("RenderString", err)
  3146  		return
  3147  	}
  3148  
  3149  	ctx.JSON(http.StatusOK, map[string]any{
  3150  		"content":     content,
  3151  		"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
  3152  	})
  3153  }
  3154  
  3155  // DeleteComment delete comment of issue
  3156  func DeleteComment(ctx *context.Context) {
  3157  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  3158  	if err != nil {
  3159  		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  3160  		return
  3161  	}
  3162  
  3163  	if err := comment.LoadIssue(ctx); err != nil {
  3164  		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  3165  		return
  3166  	}
  3167  
  3168  	if comment.Issue.RepoID != ctx.Repo.Repository.ID {
  3169  		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
  3170  		return
  3171  	}
  3172  
  3173  	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
  3174  		ctx.Error(http.StatusForbidden)
  3175  		return
  3176  	} else if !comment.Type.HasContentSupport() {
  3177  		ctx.Error(http.StatusNoContent)
  3178  		return
  3179  	}
  3180  
  3181  	if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
  3182  		ctx.ServerError("DeleteComment", err)
  3183  		return
  3184  	}
  3185  
  3186  	ctx.Status(http.StatusOK)
  3187  }
  3188  
  3189  // ChangeIssueReaction create a reaction for issue
  3190  func ChangeIssueReaction(ctx *context.Context) {
  3191  	form := web.GetForm(ctx).(*forms.ReactionForm)
  3192  	issue := GetActionIssue(ctx)
  3193  	if ctx.Written() {
  3194  		return
  3195  	}
  3196  
  3197  	if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  3198  		if log.IsTrace() {
  3199  			if ctx.IsSigned {
  3200  				issueType := "issues"
  3201  				if issue.IsPull {
  3202  					issueType = "pulls"
  3203  				}
  3204  				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  3205  					"User in Repo has Permissions: %-+v",
  3206  					ctx.Doer,
  3207  					issue.PosterID,
  3208  					issueType,
  3209  					ctx.Repo.Repository,
  3210  					ctx.Repo.Permission)
  3211  			} else {
  3212  				log.Trace("Permission Denied: Not logged in")
  3213  			}
  3214  		}
  3215  
  3216  		ctx.Error(http.StatusForbidden)
  3217  		return
  3218  	}
  3219  
  3220  	if ctx.HasError() {
  3221  		ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
  3222  		return
  3223  	}
  3224  
  3225  	switch ctx.Params(":action") {
  3226  	case "react":
  3227  		reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
  3228  		if err != nil {
  3229  			if issues_model.IsErrForbiddenIssueReaction(err) {
  3230  				ctx.ServerError("ChangeIssueReaction", err)
  3231  				return
  3232  			}
  3233  			log.Info("CreateIssueReaction: %s", err)
  3234  			break
  3235  		}
  3236  		// Reload new reactions
  3237  		issue.Reactions = nil
  3238  		if err = issue.LoadAttributes(ctx); err != nil {
  3239  			log.Info("issue.LoadAttributes: %s", err)
  3240  			break
  3241  		}
  3242  
  3243  		log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
  3244  	case "unreact":
  3245  		if err := issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content); err != nil {
  3246  			ctx.ServerError("DeleteIssueReaction", err)
  3247  			return
  3248  		}
  3249  
  3250  		// Reload new reactions
  3251  		issue.Reactions = nil
  3252  		if err := issue.LoadAttributes(ctx); err != nil {
  3253  			log.Info("issue.LoadAttributes: %s", err)
  3254  			break
  3255  		}
  3256  
  3257  		log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
  3258  	default:
  3259  		ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
  3260  		return
  3261  	}
  3262  
  3263  	if len(issue.Reactions) == 0 {
  3264  		ctx.JSON(http.StatusOK, map[string]any{
  3265  			"empty": true,
  3266  			"html":  "",
  3267  		})
  3268  		return
  3269  	}
  3270  
  3271  	html, err := ctx.RenderToString(tplReactions, map[string]any{
  3272  		"ctxData":   ctx.Data,
  3273  		"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
  3274  		"Reactions": issue.Reactions.GroupByType(),
  3275  	})
  3276  	if err != nil {
  3277  		ctx.ServerError("ChangeIssueReaction.HTMLString", err)
  3278  		return
  3279  	}
  3280  	ctx.JSON(http.StatusOK, map[string]any{
  3281  		"html": html,
  3282  	})
  3283  }
  3284  
  3285  // ChangeCommentReaction create a reaction for comment
  3286  func ChangeCommentReaction(ctx *context.Context) {
  3287  	form := web.GetForm(ctx).(*forms.ReactionForm)
  3288  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  3289  	if err != nil {
  3290  		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  3291  		return
  3292  	}
  3293  
  3294  	if err := comment.LoadIssue(ctx); err != nil {
  3295  		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  3296  		return
  3297  	}
  3298  
  3299  	if comment.Issue.RepoID != ctx.Repo.Repository.ID {
  3300  		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
  3301  		return
  3302  	}
  3303  
  3304  	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
  3305  		if log.IsTrace() {
  3306  			if ctx.IsSigned {
  3307  				issueType := "issues"
  3308  				if comment.Issue.IsPull {
  3309  					issueType = "pulls"
  3310  				}
  3311  				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
  3312  					"User in Repo has Permissions: %-+v",
  3313  					ctx.Doer,
  3314  					comment.Issue.PosterID,
  3315  					issueType,
  3316  					ctx.Repo.Repository,
  3317  					ctx.Repo.Permission)
  3318  			} else {
  3319  				log.Trace("Permission Denied: Not logged in")
  3320  			}
  3321  		}
  3322  
  3323  		ctx.Error(http.StatusForbidden)
  3324  		return
  3325  	}
  3326  
  3327  	if !comment.Type.HasContentSupport() {
  3328  		ctx.Error(http.StatusNoContent)
  3329  		return
  3330  	}
  3331  
  3332  	switch ctx.Params(":action") {
  3333  	case "react":
  3334  		reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
  3335  		if err != nil {
  3336  			if issues_model.IsErrForbiddenIssueReaction(err) {
  3337  				ctx.ServerError("ChangeIssueReaction", err)
  3338  				return
  3339  			}
  3340  			log.Info("CreateCommentReaction: %s", err)
  3341  			break
  3342  		}
  3343  		// Reload new reactions
  3344  		comment.Reactions = nil
  3345  		if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
  3346  			log.Info("comment.LoadReactions: %s", err)
  3347  			break
  3348  		}
  3349  
  3350  		log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
  3351  	case "unreact":
  3352  		if err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content); err != nil {
  3353  			ctx.ServerError("DeleteCommentReaction", err)
  3354  			return
  3355  		}
  3356  
  3357  		// Reload new reactions
  3358  		comment.Reactions = nil
  3359  		if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil {
  3360  			log.Info("comment.LoadReactions: %s", err)
  3361  			break
  3362  		}
  3363  
  3364  		log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
  3365  	default:
  3366  		ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
  3367  		return
  3368  	}
  3369  
  3370  	if len(comment.Reactions) == 0 {
  3371  		ctx.JSON(http.StatusOK, map[string]any{
  3372  			"empty": true,
  3373  			"html":  "",
  3374  		})
  3375  		return
  3376  	}
  3377  
  3378  	html, err := ctx.RenderToString(tplReactions, map[string]any{
  3379  		"ctxData":   ctx.Data,
  3380  		"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
  3381  		"Reactions": comment.Reactions.GroupByType(),
  3382  	})
  3383  	if err != nil {
  3384  		ctx.ServerError("ChangeCommentReaction.HTMLString", err)
  3385  		return
  3386  	}
  3387  	ctx.JSON(http.StatusOK, map[string]any{
  3388  		"html": html,
  3389  	})
  3390  }
  3391  
  3392  func addParticipant(poster *user_model.User, participants []*user_model.User) []*user_model.User {
  3393  	for _, part := range participants {
  3394  		if poster.ID == part.ID {
  3395  			return participants
  3396  		}
  3397  	}
  3398  	return append(participants, poster)
  3399  }
  3400  
  3401  func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
  3402  	// Remove comments that the user has no permissions to see
  3403  	for i := 0; i < len(issue.Comments); {
  3404  		c := issue.Comments[i]
  3405  		if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
  3406  			var err error
  3407  			// Set RefRepo for description in template
  3408  			c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
  3409  			if err != nil {
  3410  				return err
  3411  			}
  3412  			perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
  3413  			if err != nil {
  3414  				return err
  3415  			}
  3416  			if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
  3417  				issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
  3418  				continue
  3419  			}
  3420  		}
  3421  		i++
  3422  	}
  3423  	return nil
  3424  }
  3425  
  3426  // GetIssueAttachments returns attachments for the issue
  3427  func GetIssueAttachments(ctx *context.Context) {
  3428  	issue := GetActionIssue(ctx)
  3429  	if ctx.Written() {
  3430  		return
  3431  	}
  3432  	attachments := make([]*api.Attachment, len(issue.Attachments))
  3433  	for i := 0; i < len(issue.Attachments); i++ {
  3434  		attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i])
  3435  	}
  3436  	ctx.JSON(http.StatusOK, attachments)
  3437  }
  3438  
  3439  // GetCommentAttachments returns attachments for the comment
  3440  func GetCommentAttachments(ctx *context.Context) {
  3441  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
  3442  	if err != nil {
  3443  		ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
  3444  		return
  3445  	}
  3446  
  3447  	if err := comment.LoadIssue(ctx); err != nil {
  3448  		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
  3449  		return
  3450  	}
  3451  
  3452  	if comment.Issue.RepoID != ctx.Repo.Repository.ID {
  3453  		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
  3454  		return
  3455  	}
  3456  
  3457  	if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
  3458  		ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{})
  3459  		return
  3460  	}
  3461  
  3462  	if !comment.Type.HasAttachmentSupport() {
  3463  		ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type))
  3464  		return
  3465  	}
  3466  
  3467  	attachments := make([]*api.Attachment, 0)
  3468  	if err := comment.LoadAttachments(ctx); err != nil {
  3469  		ctx.ServerError("LoadAttachments", err)
  3470  		return
  3471  	}
  3472  	for i := 0; i < len(comment.Attachments); i++ {
  3473  		attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i]))
  3474  	}
  3475  	ctx.JSON(http.StatusOK, attachments)
  3476  }
  3477  
  3478  func updateAttachments(ctx *context.Context, item any, files []string) error {
  3479  	var attachments []*repo_model.Attachment
  3480  	switch content := item.(type) {
  3481  	case *issues_model.Issue:
  3482  		attachments = content.Attachments
  3483  	case *issues_model.Comment:
  3484  		attachments = content.Attachments
  3485  	default:
  3486  		return fmt.Errorf("unknown Type: %T", content)
  3487  	}
  3488  	for i := 0; i < len(attachments); i++ {
  3489  		if util.SliceContainsString(files, attachments[i].UUID) {
  3490  			continue
  3491  		}
  3492  		if err := repo_model.DeleteAttachment(ctx, attachments[i], true); err != nil {
  3493  			return err
  3494  		}
  3495  	}
  3496  	var err error
  3497  	if len(files) > 0 {
  3498  		switch content := item.(type) {
  3499  		case *issues_model.Issue:
  3500  			err = issues_model.UpdateIssueAttachments(ctx, content.ID, files)
  3501  		case *issues_model.Comment:
  3502  			err = content.UpdateAttachments(ctx, files)
  3503  		default:
  3504  			return fmt.Errorf("unknown Type: %T", content)
  3505  		}
  3506  		if err != nil {
  3507  			return err
  3508  		}
  3509  	}
  3510  	switch content := item.(type) {
  3511  	case *issues_model.Issue:
  3512  		content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID)
  3513  	case *issues_model.Comment:
  3514  		content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID)
  3515  	default:
  3516  		return fmt.Errorf("unknown Type: %T", content)
  3517  	}
  3518  	return err
  3519  }
  3520  
  3521  func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
  3522  	attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
  3523  		"ctxData":     ctx.Data,
  3524  		"Attachments": attachments,
  3525  		"Content":     content,
  3526  	})
  3527  	if err != nil {
  3528  		ctx.ServerError("attachmentsHTML.HTMLString", err)
  3529  		return ""
  3530  	}
  3531  	return attachHTML
  3532  }
  3533  
  3534  // combineLabelComments combine the nearby label comments as one.
  3535  func combineLabelComments(issue *issues_model.Issue) {
  3536  	var prev, cur *issues_model.Comment
  3537  	for i := 0; i < len(issue.Comments); i++ {
  3538  		cur = issue.Comments[i]
  3539  		if i > 0 {
  3540  			prev = issue.Comments[i-1]
  3541  		}
  3542  		if i == 0 || cur.Type != issues_model.CommentTypeLabel ||
  3543  			(prev != nil && prev.PosterID != cur.PosterID) ||
  3544  			(prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
  3545  			if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
  3546  				if cur.Content != "1" {
  3547  					cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
  3548  				} else {
  3549  					cur.AddedLabels = append(cur.AddedLabels, cur.Label)
  3550  				}
  3551  			}
  3552  			continue
  3553  		}
  3554  
  3555  		if cur.Label != nil { // now cur MUST be label comment
  3556  			if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
  3557  				if cur.Content != "1" {
  3558  					// remove labels from the AddedLabels list if the label that was removed is already
  3559  					// in this list, and if it's not in this list, add the label to RemovedLabels
  3560  					addedAndRemoved := false
  3561  					for i, label := range prev.AddedLabels {
  3562  						if cur.Label.ID == label.ID {
  3563  							prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...)
  3564  							addedAndRemoved = true
  3565  							break
  3566  						}
  3567  					}
  3568  					if !addedAndRemoved {
  3569  						prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
  3570  					}
  3571  				} else {
  3572  					// remove labels from the RemovedLabels list if the label that was added is already
  3573  					// in this list, and if it's not in this list, add the label to AddedLabels
  3574  					removedAndAdded := false
  3575  					for i, label := range prev.RemovedLabels {
  3576  						if cur.Label.ID == label.ID {
  3577  							prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...)
  3578  							removedAndAdded = true
  3579  							break
  3580  						}
  3581  					}
  3582  					if !removedAndAdded {
  3583  						prev.AddedLabels = append(prev.AddedLabels, cur.Label)
  3584  					}
  3585  				}
  3586  				prev.CreatedUnix = cur.CreatedUnix
  3587  				// remove the current comment since it has been combined to prev comment
  3588  				issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
  3589  				i--
  3590  			} else { // if prev is not a label comment, start a new group
  3591  				if cur.Content != "1" {
  3592  					cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
  3593  				} else {
  3594  					cur.AddedLabels = append(cur.AddedLabels, cur.Label)
  3595  				}
  3596  			}
  3597  		}
  3598  	}
  3599  }
  3600  
  3601  // get all teams that current user can mention
  3602  func handleTeamMentions(ctx *context.Context) {
  3603  	if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
  3604  		return
  3605  	}
  3606  
  3607  	var isAdmin bool
  3608  	var err error
  3609  	var teams []*organization.Team
  3610  	org := organization.OrgFromUser(ctx.Repo.Owner)
  3611  	// Admin has super access.
  3612  	if ctx.Doer.IsAdmin {
  3613  		isAdmin = true
  3614  	} else {
  3615  		isAdmin, err = org.IsOwnedBy(ctx.Doer.ID)
  3616  		if err != nil {
  3617  			ctx.ServerError("IsOwnedBy", err)
  3618  			return
  3619  		}
  3620  	}
  3621  
  3622  	if isAdmin {
  3623  		teams, err = org.LoadTeams()
  3624  		if err != nil {
  3625  			ctx.ServerError("LoadTeams", err)
  3626  			return
  3627  		}
  3628  	} else {
  3629  		teams, err = org.GetUserTeams(ctx.Doer.ID)
  3630  		if err != nil {
  3631  			ctx.ServerError("GetUserTeams", err)
  3632  			return
  3633  		}
  3634  	}
  3635  
  3636  	ctx.Data["MentionableTeams"] = teams
  3637  	ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
  3638  	ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
  3639  }
  3640  
  3641  type userSearchInfo struct {
  3642  	UserID     int64  `json:"user_id"`
  3643  	UserName   string `json:"username"`
  3644  	AvatarLink string `json:"avatar_link"`
  3645  	FullName   string `json:"full_name"`
  3646  }
  3647  
  3648  type userSearchResponse struct {
  3649  	Results []*userSearchInfo `json:"results"`
  3650  }
  3651  
  3652  // IssuePosters get posters for current repo's issues/pull requests
  3653  func IssuePosters(ctx *context.Context) {
  3654  	issuePosters(ctx, false)
  3655  }
  3656  
  3657  func PullPosters(ctx *context.Context) {
  3658  	issuePosters(ctx, true)
  3659  }
  3660  
  3661  func issuePosters(ctx *context.Context, isPullList bool) {
  3662  	repo := ctx.Repo.Repository
  3663  	search := strings.TrimSpace(ctx.FormString("q"))
  3664  	posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName)
  3665  	if err != nil {
  3666  		ctx.JSON(http.StatusInternalServerError, err)
  3667  		return
  3668  	}
  3669  
  3670  	if search == "" && ctx.Doer != nil {
  3671  		// the returned posters slice only contains limited number of users,
  3672  		// to make the current user (doer) can quickly filter their own issues, always add doer to the posters slice
  3673  		if !slices.ContainsFunc(posters, func(user *user_model.User) bool { return user.ID == ctx.Doer.ID }) {
  3674  			posters = append(posters, ctx.Doer)
  3675  		}
  3676  	}
  3677  
  3678  	posters = MakeSelfOnTop(ctx.Doer, posters)
  3679  
  3680  	resp := &userSearchResponse{}
  3681  	resp.Results = make([]*userSearchInfo, len(posters))
  3682  	for i, user := range posters {
  3683  		resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
  3684  		if setting.UI.DefaultShowFullName {
  3685  			resp.Results[i].FullName = user.FullName
  3686  		}
  3687  	}
  3688  	ctx.JSON(http.StatusOK, resp)
  3689  }