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