1// Copyright 2014 The Gogs Authors. All rights reserved.
2// Copyright 2018 The Gitea Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package repo
7
8import (
9	"bytes"
10	"errors"
11	"fmt"
12	"io"
13	"net/http"
14	"net/url"
15	"path"
16	"strconv"
17	"strings"
18
19	"code.gitea.io/gitea/models"
20	"code.gitea.io/gitea/models/db"
21	repo_model "code.gitea.io/gitea/models/repo"
22	"code.gitea.io/gitea/models/unit"
23	user_model "code.gitea.io/gitea/models/user"
24	"code.gitea.io/gitea/modules/base"
25	"code.gitea.io/gitea/modules/context"
26	"code.gitea.io/gitea/modules/convert"
27	"code.gitea.io/gitea/modules/git"
28	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
29	"code.gitea.io/gitea/modules/log"
30	"code.gitea.io/gitea/modules/markup"
31	"code.gitea.io/gitea/modules/markup/markdown"
32	"code.gitea.io/gitea/modules/setting"
33	api "code.gitea.io/gitea/modules/structs"
34	"code.gitea.io/gitea/modules/upload"
35	"code.gitea.io/gitea/modules/util"
36	"code.gitea.io/gitea/modules/web"
37	asymkey_service "code.gitea.io/gitea/services/asymkey"
38	comment_service "code.gitea.io/gitea/services/comments"
39	"code.gitea.io/gitea/services/forms"
40	issue_service "code.gitea.io/gitea/services/issue"
41	pull_service "code.gitea.io/gitea/services/pull"
42
43	"github.com/unknwon/com"
44)
45
46const (
47	tplAttachment base.TplName = "repo/issue/view_content/attachments"
48
49	tplIssues      base.TplName = "repo/issue/list"
50	tplIssueNew    base.TplName = "repo/issue/new"
51	tplIssueChoose base.TplName = "repo/issue/choose"
52	tplIssueView   base.TplName = "repo/issue/view"
53
54	tplReactions base.TplName = "repo/issue/view_content/reactions"
55
56	issueTemplateKey      = "IssueTemplate"
57	issueTemplateTitleKey = "IssueTemplateTitle"
58)
59
60var (
61	// IssueTemplateCandidates issue templates
62	IssueTemplateCandidates = []string{
63		"ISSUE_TEMPLATE.md",
64		"issue_template.md",
65		".gitea/ISSUE_TEMPLATE.md",
66		".gitea/issue_template.md",
67		".github/ISSUE_TEMPLATE.md",
68		".github/issue_template.md",
69	}
70)
71
72// MustAllowUserComment checks to make sure if an issue is locked.
73// If locked and user has permissions to write to the repository,
74// then the comment is allowed, else it is blocked
75func MustAllowUserComment(ctx *context.Context) {
76	issue := GetActionIssue(ctx)
77	if ctx.Written() {
78		return
79	}
80
81	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
82		ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
83		ctx.Redirect(issue.HTMLURL())
84		return
85	}
86}
87
88// MustEnableIssues check if repository enable internal issues
89func MustEnableIssues(ctx *context.Context) {
90	if !ctx.Repo.CanRead(unit.TypeIssues) &&
91		!ctx.Repo.CanRead(unit.TypeExternalTracker) {
92		ctx.NotFound("MustEnableIssues", nil)
93		return
94	}
95
96	unit, err := ctx.Repo.Repository.GetUnit(unit.TypeExternalTracker)
97	if err == nil {
98		ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
99		return
100	}
101}
102
103// MustAllowPulls check if repository enable pull requests and user have right to do that
104func MustAllowPulls(ctx *context.Context) {
105	if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
106		ctx.NotFound("MustAllowPulls", nil)
107		return
108	}
109
110	// User can send pull request if owns a forked repository.
111	if ctx.IsSigned && repo_model.HasForkedRepo(ctx.User.ID, ctx.Repo.Repository.ID) {
112		ctx.Repo.PullRequest.Allowed = true
113		ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.User.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName)
114	}
115}
116
117func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
118	var err error
119	viewType := ctx.FormString("type")
120	sortType := ctx.FormString("sort")
121	types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested"}
122	if !util.IsStringInSlice(viewType, types, true) {
123		viewType = "all"
124	}
125
126	var (
127		assigneeID        = ctx.FormInt64("assignee")
128		posterID          int64
129		mentionedID       int64
130		reviewRequestedID int64
131		forceEmpty        bool
132	)
133
134	if ctx.IsSigned {
135		switch viewType {
136		case "created_by":
137			posterID = ctx.User.ID
138		case "mentioned":
139			mentionedID = ctx.User.ID
140		case "assigned":
141			assigneeID = ctx.User.ID
142		case "review_requested":
143			reviewRequestedID = ctx.User.ID
144		}
145	}
146
147	repo := ctx.Repo.Repository
148	var labelIDs []int64
149	selectLabels := ctx.FormString("labels")
150	if len(selectLabels) > 0 && selectLabels != "0" {
151		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
152		if err != nil {
153			ctx.ServerError("StringsToInt64s", err)
154			return
155		}
156	}
157
158	keyword := strings.Trim(ctx.FormString("q"), " ")
159	if bytes.Contains([]byte(keyword), []byte{0x00}) {
160		keyword = ""
161	}
162
163	var issueIDs []int64
164	if len(keyword) > 0 {
165		issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{repo.ID}, keyword)
166		if err != nil {
167			ctx.ServerError("issueIndexer.Search", err)
168			return
169		}
170		if len(issueIDs) == 0 {
171			forceEmpty = true
172		}
173	}
174
175	var issueStats *models.IssueStats
176	if forceEmpty {
177		issueStats = &models.IssueStats{}
178	} else {
179		issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
180			RepoID:            repo.ID,
181			Labels:            selectLabels,
182			MilestoneID:       milestoneID,
183			AssigneeID:        assigneeID,
184			MentionedID:       mentionedID,
185			PosterID:          posterID,
186			ReviewRequestedID: reviewRequestedID,
187			IsPull:            isPullOption,
188			IssueIDs:          issueIDs,
189		})
190		if err != nil {
191			ctx.ServerError("GetIssueStats", err)
192			return
193		}
194	}
195
196	isShowClosed := ctx.FormString("state") == "closed"
197	// if open issues are zero and close don't, use closed as default
198	if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
199		isShowClosed = true
200	}
201
202	page := ctx.FormInt("page")
203	if page <= 1 {
204		page = 1
205	}
206
207	var total int
208	if !isShowClosed {
209		total = int(issueStats.OpenCount)
210	} else {
211		total = int(issueStats.ClosedCount)
212	}
213	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
214
215	var mileIDs []int64
216	if milestoneID > 0 {
217		mileIDs = []int64{milestoneID}
218	}
219
220	var issues []*models.Issue
221	if forceEmpty {
222		issues = []*models.Issue{}
223	} else {
224		issues, err = models.Issues(&models.IssuesOptions{
225			ListOptions: db.ListOptions{
226				Page:     pager.Paginater.Current(),
227				PageSize: setting.UI.IssuePagingNum,
228			},
229			RepoIDs:           []int64{repo.ID},
230			AssigneeID:        assigneeID,
231			PosterID:          posterID,
232			MentionedID:       mentionedID,
233			ReviewRequestedID: reviewRequestedID,
234			MilestoneIDs:      mileIDs,
235			ProjectID:         projectID,
236			IsClosed:          util.OptionalBoolOf(isShowClosed),
237			IsPull:            isPullOption,
238			LabelIDs:          labelIDs,
239			SortType:          sortType,
240			IssueIDs:          issueIDs,
241		})
242		if err != nil {
243			ctx.ServerError("Issues", err)
244			return
245		}
246	}
247
248	var issueList = models.IssueList(issues)
249	approvalCounts, err := issueList.GetApprovalCounts()
250	if err != nil {
251		ctx.ServerError("ApprovalCounts", err)
252		return
253	}
254
255	// Get posters.
256	for i := range issues {
257		// Check read status
258		if !ctx.IsSigned {
259			issues[i].IsRead = true
260		} else if err = issues[i].GetIsRead(ctx.User.ID); err != nil {
261			ctx.ServerError("GetIsRead", err)
262			return
263		}
264	}
265
266	commitStatus, err := pull_service.GetIssuesLastCommitStatus(issues)
267	if err != nil {
268		ctx.ServerError("GetIssuesLastCommitStatus", err)
269		return
270	}
271
272	ctx.Data["Issues"] = issues
273	ctx.Data["CommitStatus"] = commitStatus
274
275	// Get assignees.
276	ctx.Data["Assignees"], err = models.GetRepoAssignees(repo)
277	if err != nil {
278		ctx.ServerError("GetAssignees", err)
279		return
280	}
281
282	handleTeamMentions(ctx)
283	if ctx.Written() {
284		return
285	}
286
287	labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
288	if err != nil {
289		ctx.ServerError("GetLabelsByRepoID", err)
290		return
291	}
292
293	if repo.Owner.IsOrganization() {
294		orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
295		if err != nil {
296			ctx.ServerError("GetLabelsByOrgID", err)
297			return
298		}
299
300		ctx.Data["OrgLabels"] = orgLabels
301		labels = append(labels, orgLabels...)
302	}
303
304	for _, l := range labels {
305		l.LoadSelectedLabelsAfterClick(labelIDs)
306	}
307	ctx.Data["Labels"] = labels
308	ctx.Data["NumLabels"] = len(labels)
309
310	if ctx.FormInt64("assignee") == 0 {
311		assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
312	}
313
314	ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] =
315		issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
316
317	ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
318		counts, ok := approvalCounts[issueID]
319		if !ok || len(counts) == 0 {
320			return 0
321		}
322		reviewTyp := models.ReviewTypeApprove
323		if typ == "reject" {
324			reviewTyp = models.ReviewTypeReject
325		} else if typ == "waiting" {
326			reviewTyp = models.ReviewTypeRequest
327		}
328		for _, count := range counts {
329			if count.Type == reviewTyp {
330				return count.Count
331			}
332		}
333		return 0
334	}
335
336	if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") {
337		projects, _, err := models.GetProjects(models.ProjectSearchOptions{
338			RepoID:   repo.ID,
339			Type:     models.ProjectTypeRepository,
340			IsClosed: util.OptionalBoolOf(isShowClosed),
341		})
342		if err != nil {
343			ctx.ServerError("GetProjects", err)
344			return
345		}
346		ctx.Data["Projects"] = projects
347	}
348
349	ctx.Data["IssueStats"] = issueStats
350	ctx.Data["SelLabelIDs"] = labelIDs
351	ctx.Data["SelectLabels"] = selectLabels
352	ctx.Data["ViewType"] = viewType
353	ctx.Data["SortType"] = sortType
354	ctx.Data["MilestoneID"] = milestoneID
355	ctx.Data["AssigneeID"] = assigneeID
356	ctx.Data["IsShowClosed"] = isShowClosed
357	ctx.Data["Keyword"] = keyword
358	if isShowClosed {
359		ctx.Data["State"] = "closed"
360	} else {
361		ctx.Data["State"] = "open"
362	}
363
364	pager.AddParam(ctx, "q", "Keyword")
365	pager.AddParam(ctx, "type", "ViewType")
366	pager.AddParam(ctx, "sort", "SortType")
367	pager.AddParam(ctx, "state", "State")
368	pager.AddParam(ctx, "labels", "SelectLabels")
369	pager.AddParam(ctx, "milestone", "MilestoneID")
370	pager.AddParam(ctx, "assignee", "AssigneeID")
371	ctx.Data["Page"] = pager
372}
373
374// Issues render issues page
375func Issues(ctx *context.Context) {
376	isPullList := ctx.Params(":type") == "pulls"
377	if isPullList {
378		MustAllowPulls(ctx)
379		if ctx.Written() {
380			return
381		}
382		ctx.Data["Title"] = ctx.Tr("repo.pulls")
383		ctx.Data["PageIsPullList"] = true
384	} else {
385		MustEnableIssues(ctx)
386		if ctx.Written() {
387			return
388		}
389		ctx.Data["Title"] = ctx.Tr("repo.issues")
390		ctx.Data["PageIsIssueList"] = true
391		ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
392	}
393
394	issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
395	if ctx.Written() {
396		return
397	}
398
399	var err error
400	// Get milestones
401	ctx.Data["Milestones"], _, err = models.GetMilestones(models.GetMilestonesOption{
402		RepoID: ctx.Repo.Repository.ID,
403		State:  api.StateType(ctx.FormString("state")),
404	})
405	if err != nil {
406		ctx.ServerError("GetAllRepoMilestones", err)
407		return
408	}
409
410	ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
411
412	ctx.HTML(http.StatusOK, tplIssues)
413}
414
415// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
416func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) {
417	var err error
418	ctx.Data["OpenMilestones"], _, err = models.GetMilestones(models.GetMilestonesOption{
419		RepoID: repo.ID,
420		State:  api.StateOpen,
421	})
422	if err != nil {
423		ctx.ServerError("GetMilestones", err)
424		return
425	}
426	ctx.Data["ClosedMilestones"], _, err = models.GetMilestones(models.GetMilestonesOption{
427		RepoID: repo.ID,
428		State:  api.StateClosed,
429	})
430	if err != nil {
431		ctx.ServerError("GetMilestones", err)
432		return
433	}
434
435	ctx.Data["Assignees"], err = models.GetRepoAssignees(repo)
436	if err != nil {
437		ctx.ServerError("GetAssignees", err)
438		return
439	}
440
441	handleTeamMentions(ctx)
442}
443
444func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
445
446	var err error
447
448	ctx.Data["OpenProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
449		RepoID:   repo.ID,
450		Page:     -1,
451		IsClosed: util.OptionalBoolFalse,
452		Type:     models.ProjectTypeRepository,
453	})
454	if err != nil {
455		ctx.ServerError("GetProjects", err)
456		return
457	}
458
459	ctx.Data["ClosedProjects"], _, err = models.GetProjects(models.ProjectSearchOptions{
460		RepoID:   repo.ID,
461		Page:     -1,
462		IsClosed: util.OptionalBoolTrue,
463		Type:     models.ProjectTypeRepository,
464	})
465	if err != nil {
466		ctx.ServerError("GetProjects", err)
467		return
468	}
469}
470
471// repoReviewerSelection items to bee shown
472type repoReviewerSelection struct {
473	IsTeam    bool
474	Team      *models.Team
475	User      *user_model.User
476	Review    *models.Review
477	CanChange bool
478	Checked   bool
479	ItemID    int64
480}
481
482// RetrieveRepoReviewers find all reviewers of a repository
483func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *models.Issue, canChooseReviewer bool) {
484	ctx.Data["CanChooseReviewer"] = canChooseReviewer
485
486	originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
487	if err != nil {
488		ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
489		return
490	}
491	ctx.Data["OriginalReviews"] = originalAuthorReviews
492
493	reviews, err := models.GetReviewersByIssueID(issue.ID)
494	if err != nil {
495		ctx.ServerError("GetReviewersByIssueID", err)
496		return
497	}
498
499	if len(reviews) == 0 && !canChooseReviewer {
500		return
501	}
502
503	var (
504		pullReviews         []*repoReviewerSelection
505		reviewersResult     []*repoReviewerSelection
506		teamReviewersResult []*repoReviewerSelection
507		teamReviewers       []*models.Team
508		reviewers           []*user_model.User
509	)
510
511	if canChooseReviewer {
512		posterID := issue.PosterID
513		if issue.OriginalAuthorID > 0 {
514			posterID = 0
515		}
516
517		reviewers, err = models.GetReviewers(repo, ctx.User.ID, posterID)
518		if err != nil {
519			ctx.ServerError("GetReviewers", err)
520			return
521		}
522
523		teamReviewers, err = models.GetReviewerTeams(repo)
524		if err != nil {
525			ctx.ServerError("GetReviewerTeams", err)
526			return
527		}
528
529		if len(reviewers) > 0 {
530			reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
531		}
532
533		if len(teamReviewers) > 0 {
534			teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
535		}
536	}
537
538	pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
539
540	for _, review := range reviews {
541		tmp := &repoReviewerSelection{
542			Checked: review.Type == models.ReviewTypeRequest,
543			Review:  review,
544			ItemID:  review.ReviewerID,
545		}
546		if review.ReviewerTeamID > 0 {
547			tmp.IsTeam = true
548			tmp.ItemID = -review.ReviewerTeamID
549		}
550
551		if ctx.Repo.IsAdmin() {
552			// Admin can dismiss or re-request any review requests
553			tmp.CanChange = true
554		} else if ctx.User != nil && ctx.User.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
555			// A user can refuse review requests
556			tmp.CanChange = true
557		} else if (canChooseReviewer || (ctx.User != nil && ctx.User.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
558			ctx.User.ID != review.ReviewerID {
559			// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
560			tmp.CanChange = true
561		}
562
563		pullReviews = append(pullReviews, tmp)
564
565		if canChooseReviewer {
566			if tmp.IsTeam {
567				teamReviewersResult = append(teamReviewersResult, tmp)
568			} else {
569				reviewersResult = append(reviewersResult, tmp)
570			}
571		}
572	}
573
574	if len(pullReviews) > 0 {
575		// Drop all non-existing users and teams from the reviews
576		currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
577		for _, item := range pullReviews {
578			if item.Review.ReviewerID > 0 {
579				if err = item.Review.LoadReviewer(); err != nil {
580					if user_model.IsErrUserNotExist(err) {
581						continue
582					}
583					ctx.ServerError("LoadReviewer", err)
584					return
585				}
586				item.User = item.Review.Reviewer
587			} else if item.Review.ReviewerTeamID > 0 {
588				if err = item.Review.LoadReviewerTeam(); err != nil {
589					if models.IsErrTeamNotExist(err) {
590						continue
591					}
592					ctx.ServerError("LoadReviewerTeam", err)
593					return
594				}
595				item.Team = item.Review.ReviewerTeam
596			} else {
597				continue
598			}
599
600			currentPullReviewers = append(currentPullReviewers, item)
601		}
602		ctx.Data["PullReviewers"] = currentPullReviewers
603	}
604
605	if canChooseReviewer && reviewersResult != nil {
606		preadded := len(reviewersResult)
607		for _, reviewer := range reviewers {
608			found := false
609		reviewAddLoop:
610			for _, tmp := range reviewersResult[:preadded] {
611				if tmp.ItemID == reviewer.ID {
612					tmp.User = reviewer
613					found = true
614					break reviewAddLoop
615				}
616			}
617
618			if found {
619				continue
620			}
621
622			reviewersResult = append(reviewersResult, &repoReviewerSelection{
623				IsTeam:    false,
624				CanChange: true,
625				User:      reviewer,
626				ItemID:    reviewer.ID,
627			})
628		}
629
630		ctx.Data["Reviewers"] = reviewersResult
631	}
632
633	if canChooseReviewer && teamReviewersResult != nil {
634		preadded := len(teamReviewersResult)
635		for _, team := range teamReviewers {
636			found := false
637		teamReviewAddLoop:
638			for _, tmp := range teamReviewersResult[:preadded] {
639				if tmp.ItemID == -team.ID {
640					tmp.Team = team
641					found = true
642					break teamReviewAddLoop
643				}
644			}
645
646			if found {
647				continue
648			}
649
650			teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
651				IsTeam:    true,
652				CanChange: true,
653				Team:      team,
654				ItemID:    -team.ID,
655			})
656		}
657
658		ctx.Data["TeamReviewers"] = teamReviewersResult
659	}
660}
661
662// RetrieveRepoMetas find all the meta information of a repository
663func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*models.Label {
664	if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
665		return nil
666	}
667
668	labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
669	if err != nil {
670		ctx.ServerError("GetLabelsByRepoID", err)
671		return nil
672	}
673	ctx.Data["Labels"] = labels
674	if repo.Owner.IsOrganization() {
675		orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
676		if err != nil {
677			return nil
678		}
679
680		ctx.Data["OrgLabels"] = orgLabels
681		labels = append(labels, orgLabels...)
682	}
683
684	RetrieveRepoMilestonesAndAssignees(ctx, repo)
685	if ctx.Written() {
686		return nil
687	}
688
689	retrieveProjects(ctx, repo)
690	if ctx.Written() {
691		return nil
692	}
693
694	brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0)
695	if err != nil {
696		ctx.ServerError("GetBranches", err)
697		return nil
698	}
699	ctx.Data["Branches"] = brs
700
701	// Contains true if the user can create issue dependencies
702	ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
703
704	return labels
705}
706
707func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
708	var bytes []byte
709
710	if ctx.Repo.Commit == nil {
711		var err error
712		ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
713		if err != nil {
714			return "", false
715		}
716	}
717
718	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
719	if err != nil {
720		return "", false
721	}
722	if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
723		return "", false
724	}
725	r, err := entry.Blob().DataAsync()
726	if err != nil {
727		return "", false
728	}
729	defer r.Close()
730	bytes, err = io.ReadAll(r)
731	if err != nil {
732		return "", false
733	}
734	return string(bytes), true
735}
736
737func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) {
738	templateCandidates := make([]string, 0, len(possibleFiles))
739	if ctx.FormString("template") != "" {
740		for _, dirName := range possibleDirs {
741			templateCandidates = append(templateCandidates, path.Join(dirName, ctx.FormString("template")))
742		}
743	}
744	templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
745	for _, filename := range templateCandidates {
746		templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
747		if found {
748			var meta api.IssueTemplate
749			templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
750			if err != nil {
751				log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
752				ctx.Data[ctxDataKey] = templateContent
753				return
754			}
755			ctx.Data[issueTemplateTitleKey] = meta.Title
756			ctx.Data[ctxDataKey] = templateBody
757			labelIDs := make([]string, 0, len(meta.Labels))
758			if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
759				ctx.Data["Labels"] = repoLabels
760				if ctx.Repo.Owner.IsOrganization() {
761					if orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
762						ctx.Data["OrgLabels"] = orgLabels
763						repoLabels = append(repoLabels, orgLabels...)
764					}
765				}
766
767				for _, metaLabel := range meta.Labels {
768					for _, repoLabel := range repoLabels {
769						if strings.EqualFold(repoLabel.Name, metaLabel) {
770							repoLabel.IsChecked = true
771							labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
772							break
773						}
774					}
775				}
776			}
777			ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
778			ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
779			ctx.Data["Reference"] = meta.Ref
780			ctx.Data["RefEndName"] = git.RefEndName(meta.Ref)
781			return
782		}
783	}
784}
785
786// NewIssue render creating issue page
787func NewIssue(ctx *context.Context) {
788	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
789	ctx.Data["PageIsIssueList"] = true
790	ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
791	ctx.Data["RequireHighlightJS"] = true
792	ctx.Data["RequireTribute"] = true
793	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
794	title := ctx.FormString("title")
795	ctx.Data["TitleQuery"] = title
796	body := ctx.FormString("body")
797	ctx.Data["BodyQuery"] = body
798
799	ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
800	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
801	upload.AddUploadContext(ctx, "comment")
802
803	milestoneID := ctx.FormInt64("milestone")
804	if milestoneID > 0 {
805		milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID)
806		if err != nil {
807			log.Error("GetMilestoneByID: %d: %v", milestoneID, err)
808		} else {
809			ctx.Data["milestone_id"] = milestoneID
810			ctx.Data["Milestone"] = milestone
811		}
812	}
813
814	projectID := ctx.FormInt64("project")
815	if projectID > 0 {
816		project, err := models.GetProjectByID(projectID)
817		if err != nil {
818			log.Error("GetProjectByID: %d: %v", projectID, err)
819		} else if project.RepoID != ctx.Repo.Repository.ID {
820			log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID))
821		} else {
822			ctx.Data["project_id"] = projectID
823			ctx.Data["Project"] = project
824		}
825
826		if len(ctx.Req.URL.Query().Get("project")) > 0 {
827			ctx.Data["redirect_after_creation"] = "project"
828		}
829	}
830
831	RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
832	setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
833	if ctx.Written() {
834		return
835	}
836
837	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
838
839	ctx.HTML(http.StatusOK, tplIssueNew)
840}
841
842// NewIssueChooseTemplate render creating issue from template page
843func NewIssueChooseTemplate(ctx *context.Context) {
844	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
845	ctx.Data["PageIsIssueList"] = true
846
847	issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
848	ctx.Data["IssueTemplates"] = issueTemplates
849
850	if len(issueTemplates) == 0 {
851		// 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.
852		ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.HTMLURL(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
853		return
854	}
855
856	ctx.Data["milestone"] = ctx.FormInt64("milestone")
857	ctx.Data["project"] = ctx.FormInt64("project")
858
859	ctx.HTML(http.StatusOK, tplIssueChoose)
860}
861
862// ValidateRepoMetas check and returns repository's meta information
863func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) {
864	var (
865		repo = ctx.Repo.Repository
866		err  error
867	)
868
869	labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
870	if ctx.Written() {
871		return nil, nil, 0, 0
872	}
873
874	var labelIDs []int64
875	hasSelected := false
876	// Check labels.
877	if len(form.LabelIDs) > 0 {
878		labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
879		if err != nil {
880			return nil, nil, 0, 0
881		}
882		labelIDMark := base.Int64sToMap(labelIDs)
883
884		for i := range labels {
885			if labelIDMark[labels[i].ID] {
886				labels[i].IsChecked = true
887				hasSelected = true
888			}
889		}
890	}
891
892	ctx.Data["Labels"] = labels
893	ctx.Data["HasSelectedLabel"] = hasSelected
894	ctx.Data["label_ids"] = form.LabelIDs
895
896	// Check milestone.
897	milestoneID := form.MilestoneID
898	if milestoneID > 0 {
899		milestone, err := models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, milestoneID)
900		if err != nil {
901			ctx.ServerError("GetMilestoneByID", err)
902			return nil, nil, 0, 0
903		}
904		if milestone.RepoID != repo.ID {
905			ctx.ServerError("GetMilestoneByID", err)
906			return nil, nil, 0, 0
907		}
908		ctx.Data["Milestone"] = milestone
909		ctx.Data["milestone_id"] = milestoneID
910	}
911
912	if form.ProjectID > 0 {
913		p, err := models.GetProjectByID(form.ProjectID)
914		if err != nil {
915			ctx.ServerError("GetProjectByID", err)
916			return nil, nil, 0, 0
917		}
918		if p.RepoID != ctx.Repo.Repository.ID {
919			ctx.NotFound("", nil)
920			return nil, nil, 0, 0
921		}
922
923		ctx.Data["Project"] = p
924		ctx.Data["project_id"] = form.ProjectID
925	}
926
927	// Check assignees
928	var assigneeIDs []int64
929	if len(form.AssigneeIDs) > 0 {
930		assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
931		if err != nil {
932			return nil, nil, 0, 0
933		}
934
935		// Check if the passed assignees actually exists and is assignable
936		for _, aID := range assigneeIDs {
937			assignee, err := user_model.GetUserByID(aID)
938			if err != nil {
939				ctx.ServerError("GetUserByID", err)
940				return nil, nil, 0, 0
941			}
942
943			valid, err := models.CanBeAssigned(assignee, repo, isPull)
944			if err != nil {
945				ctx.ServerError("CanBeAssigned", err)
946				return nil, nil, 0, 0
947			}
948
949			if !valid {
950				ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
951				return nil, nil, 0, 0
952			}
953		}
954	}
955
956	// Keep the old assignee id thingy for compatibility reasons
957	if form.AssigneeID > 0 {
958		assigneeIDs = append(assigneeIDs, form.AssigneeID)
959	}
960
961	return labelIDs, assigneeIDs, milestoneID, form.ProjectID
962}
963
964// NewIssuePost response for creating new issue
965func NewIssuePost(ctx *context.Context) {
966	form := web.GetForm(ctx).(*forms.CreateIssueForm)
967	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
968	ctx.Data["PageIsIssueList"] = true
969	ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
970	ctx.Data["RequireHighlightJS"] = true
971	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
972	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
973	upload.AddUploadContext(ctx, "comment")
974
975	var (
976		repo        = ctx.Repo.Repository
977		attachments []string
978	)
979
980	labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false)
981	if ctx.Written() {
982		return
983	}
984
985	if setting.Attachment.Enabled {
986		attachments = form.Files
987	}
988
989	if ctx.HasError() {
990		ctx.HTML(http.StatusOK, tplIssueNew)
991		return
992	}
993
994	if util.IsEmptyString(form.Title) {
995		ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplIssueNew, form)
996		return
997	}
998
999	issue := &models.Issue{
1000		RepoID:      repo.ID,
1001		Repo:        repo,
1002		Title:       form.Title,
1003		PosterID:    ctx.User.ID,
1004		Poster:      ctx.User,
1005		MilestoneID: milestoneID,
1006		Content:     form.Content,
1007		Ref:         form.Ref,
1008	}
1009
1010	if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
1011		if models.IsErrUserDoesNotHaveAccessToRepo(err) {
1012			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
1013			return
1014		}
1015		ctx.ServerError("NewIssue", err)
1016		return
1017	}
1018
1019	if projectID > 0 {
1020		if err := models.ChangeProjectAssign(issue, ctx.User, projectID); err != nil {
1021			ctx.ServerError("ChangeProjectAssign", err)
1022			return
1023		}
1024	}
1025
1026	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
1027	if ctx.FormString("redirect_after_creation") == "project" {
1028		ctx.Redirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(form.ProjectID, 10))
1029	} else {
1030		ctx.Redirect(issue.Link())
1031	}
1032}
1033
1034// roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue
1035func roleDescriptor(repo *repo_model.Repository, poster *user_model.User, issue *models.Issue) (models.RoleDescriptor, error) {
1036	perm, err := models.GetUserRepoPermission(repo, poster)
1037	if err != nil {
1038		return models.RoleDescriptorNone, err
1039	}
1040
1041	// By default the poster has no roles on the comment.
1042	roleDescriptor := models.RoleDescriptorNone
1043
1044	// Check if the poster is owner of the repo.
1045	if perm.IsOwner() {
1046		// If the poster isn't a admin, enable the owner role.
1047		if !poster.IsAdmin {
1048			roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner)
1049		} else {
1050
1051			// Otherwise check if poster is the real repo admin.
1052			ok, err := models.IsUserRealRepoAdmin(repo, poster)
1053			if err != nil {
1054				return models.RoleDescriptorNone, err
1055			}
1056			if ok {
1057				roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner)
1058			}
1059		}
1060	}
1061
1062	// Is the poster can write issues or pulls to the repo, enable the Writer role.
1063	// Only enable this if the poster doesn't have the owner role already.
1064	if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) {
1065		roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorWriter)
1066	}
1067
1068	// If the poster is the actual poster of the issue, enable Poster role.
1069	if issue.IsPoster(poster.ID) {
1070		roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorPoster)
1071	}
1072
1073	return roleDescriptor, nil
1074}
1075
1076func getBranchData(ctx *context.Context, issue *models.Issue) {
1077	ctx.Data["BaseBranch"] = nil
1078	ctx.Data["HeadBranch"] = nil
1079	ctx.Data["HeadUserName"] = nil
1080	ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
1081	if issue.IsPull {
1082		pull := issue.PullRequest
1083		ctx.Data["BaseBranch"] = pull.BaseBranch
1084		ctx.Data["HeadBranch"] = pull.HeadBranch
1085		ctx.Data["HeadUserName"] = pull.MustHeadUserName()
1086	}
1087}
1088
1089// ViewIssue render issue view page
1090func ViewIssue(ctx *context.Context) {
1091	if ctx.Params(":type") == "issues" {
1092		// If issue was requested we check if repo has external tracker and redirect
1093		extIssueUnit, err := ctx.Repo.Repository.GetUnit(unit.TypeExternalTracker)
1094		if err == nil && extIssueUnit != nil {
1095			if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
1096				metas := ctx.Repo.Repository.ComposeMetas()
1097				metas["index"] = ctx.Params(":index")
1098				ctx.Redirect(com.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas))
1099				return
1100			}
1101		} else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
1102			ctx.ServerError("GetUnit", err)
1103			return
1104		}
1105	}
1106
1107	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
1108	if err != nil {
1109		if models.IsErrIssueNotExist(err) {
1110			ctx.NotFound("GetIssueByIndex", err)
1111		} else {
1112			ctx.ServerError("GetIssueByIndex", err)
1113		}
1114		return
1115	}
1116	if issue.Repo == nil {
1117		issue.Repo = ctx.Repo.Repository
1118	}
1119
1120	// Make sure type and URL matches.
1121	if ctx.Params(":type") == "issues" && issue.IsPull {
1122		ctx.Redirect(issue.Link())
1123		return
1124	} else if ctx.Params(":type") == "pulls" && !issue.IsPull {
1125		ctx.Redirect(issue.Link())
1126		return
1127	}
1128
1129	if issue.IsPull {
1130		MustAllowPulls(ctx)
1131		if ctx.Written() {
1132			return
1133		}
1134		ctx.Data["PageIsPullList"] = true
1135		ctx.Data["PageIsPullConversation"] = true
1136	} else {
1137		MustEnableIssues(ctx)
1138		if ctx.Written() {
1139			return
1140		}
1141		ctx.Data["PageIsIssueList"] = true
1142		ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
1143	}
1144
1145	if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
1146		ctx.Data["IssueType"] = "pulls"
1147	} else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
1148		ctx.Data["IssueType"] = "issues"
1149	} else {
1150		ctx.Data["IssueType"] = "all"
1151	}
1152
1153	ctx.Data["RequireHighlightJS"] = true
1154	ctx.Data["RequireTribute"] = true
1155	ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
1156	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
1157	upload.AddUploadContext(ctx, "comment")
1158
1159	if err = issue.LoadAttributes(); err != nil {
1160		ctx.ServerError("LoadAttributes", err)
1161		return
1162	}
1163
1164	if err = filterXRefComments(ctx, issue); err != nil {
1165		ctx.ServerError("filterXRefComments", err)
1166		return
1167	}
1168
1169	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
1170
1171	iw := new(models.IssueWatch)
1172	if ctx.User != nil {
1173		iw.UserID = ctx.User.ID
1174		iw.IssueID = issue.ID
1175		iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue)
1176		if err != nil {
1177			ctx.ServerError("CheckIssueWatch", err)
1178			return
1179		}
1180	}
1181	ctx.Data["IssueWatch"] = iw
1182
1183	issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
1184		URLPrefix: ctx.Repo.RepoLink,
1185		Metas:     ctx.Repo.Repository.ComposeMetas(),
1186		GitRepo:   ctx.Repo.GitRepo,
1187		Ctx:       ctx,
1188	}, issue.Content)
1189	if err != nil {
1190		ctx.ServerError("RenderString", err)
1191		return
1192	}
1193
1194	repo := ctx.Repo.Repository
1195
1196	// Get more information if it's a pull request.
1197	if issue.IsPull {
1198		if issue.PullRequest.HasMerged {
1199			ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
1200			PrepareMergedViewPullInfo(ctx, issue)
1201		} else {
1202			PrepareViewPullInfo(ctx, issue)
1203			ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
1204		}
1205		if ctx.Written() {
1206			return
1207		}
1208	}
1209
1210	// Metas.
1211	// Check labels.
1212	labelIDMark := make(map[int64]bool)
1213	for i := range issue.Labels {
1214		labelIDMark[issue.Labels[i].ID] = true
1215	}
1216	labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
1217	if err != nil {
1218		ctx.ServerError("GetLabelsByRepoID", err)
1219		return
1220	}
1221	ctx.Data["Labels"] = labels
1222
1223	if repo.Owner.IsOrganization() {
1224		orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
1225		if err != nil {
1226			ctx.ServerError("GetLabelsByOrgID", err)
1227			return
1228		}
1229		ctx.Data["OrgLabels"] = orgLabels
1230
1231		labels = append(labels, orgLabels...)
1232	}
1233
1234	hasSelected := false
1235	for i := range labels {
1236		if labelIDMark[labels[i].ID] {
1237			labels[i].IsChecked = true
1238			hasSelected = true
1239		}
1240	}
1241	ctx.Data["HasSelectedLabel"] = hasSelected
1242
1243	// Check milestone and assignee.
1244	if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
1245		RetrieveRepoMilestonesAndAssignees(ctx, repo)
1246		retrieveProjects(ctx, repo)
1247
1248		if ctx.Written() {
1249			return
1250		}
1251	}
1252
1253	if issue.IsPull {
1254		canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
1255		if !canChooseReviewer && ctx.User != nil && ctx.IsSigned {
1256			canChooseReviewer, err = models.IsOfficialReviewer(issue, ctx.User)
1257			if err != nil {
1258				ctx.ServerError("IsOfficialReviewer", err)
1259				return
1260			}
1261		}
1262
1263		RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
1264		if ctx.Written() {
1265			return
1266		}
1267	}
1268
1269	if ctx.IsSigned {
1270		// Update issue-user.
1271		if err = issue.ReadBy(ctx.User.ID); err != nil {
1272			ctx.ServerError("ReadBy", err)
1273			return
1274		}
1275	}
1276
1277	var (
1278		role         models.RoleDescriptor
1279		ok           bool
1280		marked       = make(map[int64]models.RoleDescriptor)
1281		comment      *models.Comment
1282		participants = make([]*user_model.User, 1, 10)
1283	)
1284	if ctx.Repo.Repository.IsTimetrackerEnabled() {
1285		if ctx.IsSigned {
1286			// Deal with the stopwatch
1287			ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
1288			if !ctx.Data["IsStopwatchRunning"].(bool) {
1289				var exists bool
1290				var sw *models.Stopwatch
1291				if exists, sw, err = models.HasUserStopwatch(ctx.User.ID); err != nil {
1292					ctx.ServerError("HasUserStopwatch", err)
1293					return
1294				}
1295				ctx.Data["HasUserStopwatch"] = exists
1296				if exists {
1297					// Add warning if the user has already a stopwatch
1298					var otherIssue *models.Issue
1299					if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
1300						ctx.ServerError("GetIssueByID", err)
1301						return
1302					}
1303					if err = otherIssue.LoadRepo(); err != nil {
1304						ctx.ServerError("LoadRepo", err)
1305						return
1306					}
1307					// Add link to the issue of the already running stopwatch
1308					ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
1309				}
1310			}
1311			ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
1312		} else {
1313			ctx.Data["CanUseTimetracker"] = false
1314		}
1315		if ctx.Data["WorkingUsers"], err = models.TotalTimes(&models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
1316			ctx.ServerError("TotalTimes", err)
1317			return
1318		}
1319	}
1320
1321	// Check if the user can use the dependencies
1322	ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, issue.IsPull)
1323
1324	// check if dependencies can be created across repositories
1325	ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
1326
1327	if issue.ShowRole, err = roleDescriptor(repo, issue.Poster, issue); err != nil {
1328		ctx.ServerError("roleDescriptor", err)
1329		return
1330	}
1331	marked[issue.PosterID] = issue.ShowRole
1332
1333	// Render comments and and fetch participants.
1334	participants[0] = issue.Poster
1335	for _, comment = range issue.Comments {
1336		comment.Issue = issue
1337
1338		if err := comment.LoadPoster(); err != nil {
1339			ctx.ServerError("LoadPoster", err)
1340			return
1341		}
1342
1343		if comment.Type == models.CommentTypeComment || comment.Type == models.CommentTypeReview {
1344			if err := comment.LoadAttachments(); err != nil {
1345				ctx.ServerError("LoadAttachments", err)
1346				return
1347			}
1348
1349			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
1350				URLPrefix: ctx.Repo.RepoLink,
1351				Metas:     ctx.Repo.Repository.ComposeMetas(),
1352				GitRepo:   ctx.Repo.GitRepo,
1353				Ctx:       ctx,
1354			}, comment.Content)
1355			if err != nil {
1356				ctx.ServerError("RenderString", err)
1357				return
1358			}
1359			// Check tag.
1360			role, ok = marked[comment.PosterID]
1361			if ok {
1362				comment.ShowRole = role
1363				continue
1364			}
1365
1366			comment.ShowRole, err = roleDescriptor(repo, comment.Poster, issue)
1367			if err != nil {
1368				ctx.ServerError("roleDescriptor", err)
1369				return
1370			}
1371			marked[comment.PosterID] = comment.ShowRole
1372			participants = addParticipant(comment.Poster, participants)
1373		} else if comment.Type == models.CommentTypeLabel {
1374			if err = comment.LoadLabel(); err != nil {
1375				ctx.ServerError("LoadLabel", err)
1376				return
1377			}
1378		} else if comment.Type == models.CommentTypeMilestone {
1379			if err = comment.LoadMilestone(); err != nil {
1380				ctx.ServerError("LoadMilestone", err)
1381				return
1382			}
1383			ghostMilestone := &models.Milestone{
1384				ID:   -1,
1385				Name: ctx.Tr("repo.issues.deleted_milestone"),
1386			}
1387			if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
1388				comment.OldMilestone = ghostMilestone
1389			}
1390			if comment.MilestoneID > 0 && comment.Milestone == nil {
1391				comment.Milestone = ghostMilestone
1392			}
1393		} else if comment.Type == models.CommentTypeProject {
1394
1395			if err = comment.LoadProject(); err != nil {
1396				ctx.ServerError("LoadProject", err)
1397				return
1398			}
1399
1400			ghostProject := &models.Project{
1401				ID:    -1,
1402				Title: ctx.Tr("repo.issues.deleted_project"),
1403			}
1404
1405			if comment.OldProjectID > 0 && comment.OldProject == nil {
1406				comment.OldProject = ghostProject
1407			}
1408
1409			if comment.ProjectID > 0 && comment.Project == nil {
1410				comment.Project = ghostProject
1411			}
1412
1413		} else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
1414			if err = comment.LoadAssigneeUserAndTeam(); err != nil {
1415				ctx.ServerError("LoadAssigneeUserAndTeam", err)
1416				return
1417			}
1418		} else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
1419			if err = comment.LoadDepIssueDetails(); err != nil {
1420				if !models.IsErrIssueNotExist(err) {
1421					ctx.ServerError("LoadDepIssueDetails", err)
1422					return
1423				}
1424			}
1425		} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
1426			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
1427				URLPrefix: ctx.Repo.RepoLink,
1428				Metas:     ctx.Repo.Repository.ComposeMetas(),
1429				GitRepo:   ctx.Repo.GitRepo,
1430				Ctx:       ctx,
1431			}, comment.Content)
1432			if err != nil {
1433				ctx.ServerError("RenderString", err)
1434				return
1435			}
1436			if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
1437				ctx.ServerError("LoadReview", err)
1438				return
1439			}
1440			participants = addParticipant(comment.Poster, participants)
1441			if comment.Review == nil {
1442				continue
1443			}
1444			if err = comment.Review.LoadAttributes(); err != nil {
1445				if !user_model.IsErrUserNotExist(err) {
1446					ctx.ServerError("Review.LoadAttributes", err)
1447					return
1448				}
1449				comment.Review.Reviewer = user_model.NewGhostUser()
1450			}
1451			if err = comment.Review.LoadCodeComments(); err != nil {
1452				ctx.ServerError("Review.LoadCodeComments", err)
1453				return
1454			}
1455			for _, codeComments := range comment.Review.CodeComments {
1456				for _, lineComments := range codeComments {
1457					for _, c := range lineComments {
1458						// Check tag.
1459						role, ok = marked[c.PosterID]
1460						if ok {
1461							c.ShowRole = role
1462							continue
1463						}
1464
1465						c.ShowRole, err = roleDescriptor(repo, c.Poster, issue)
1466						if err != nil {
1467							ctx.ServerError("roleDescriptor", err)
1468							return
1469						}
1470						marked[c.PosterID] = c.ShowRole
1471						participants = addParticipant(c.Poster, participants)
1472					}
1473				}
1474			}
1475			if err = comment.LoadResolveDoer(); err != nil {
1476				ctx.ServerError("LoadResolveDoer", err)
1477				return
1478			}
1479		} else if comment.Type == models.CommentTypePullPush {
1480			participants = addParticipant(comment.Poster, participants)
1481			if err = comment.LoadPushCommits(); err != nil {
1482				ctx.ServerError("LoadPushCommits", err)
1483				return
1484			}
1485		} else if comment.Type == models.CommentTypeAddTimeManual ||
1486			comment.Type == models.CommentTypeStopTracking {
1487			// drop error since times could be pruned from DB..
1488			_ = comment.LoadTime()
1489		}
1490	}
1491
1492	// Combine multiple label assignments into a single comment
1493	combineLabelComments(issue)
1494
1495	getBranchData(ctx, issue)
1496	if issue.IsPull {
1497		pull := issue.PullRequest
1498		pull.Issue = issue
1499		canDelete := false
1500		ctx.Data["AllowMerge"] = false
1501
1502		if ctx.IsSigned {
1503			if err := pull.LoadHeadRepo(); err != nil {
1504				log.Error("LoadHeadRepo: %v", err)
1505			} else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
1506				perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.User)
1507				if err != nil {
1508					ctx.ServerError("GetUserRepoPermission", err)
1509					return
1510				}
1511				if perm.CanWrite(unit.TypeCode) {
1512					// Check if branch is not protected
1513					if protected, err := models.IsProtectedBranch(pull.HeadRepo.ID, pull.HeadBranch); err != nil {
1514						log.Error("IsProtectedBranch: %v", err)
1515					} else if !protected {
1516						canDelete = true
1517						ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
1518					}
1519				}
1520			}
1521
1522			if err := pull.LoadBaseRepo(); err != nil {
1523				log.Error("LoadBaseRepo: %v", err)
1524			}
1525			perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.User)
1526			if err != nil {
1527				ctx.ServerError("GetUserRepoPermission", err)
1528				return
1529			}
1530			ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.User)
1531			if err != nil {
1532				ctx.ServerError("IsUserAllowedToMerge", err)
1533				return
1534			}
1535
1536			if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.User); err != nil {
1537				ctx.ServerError("CanMarkConversation", err)
1538				return
1539			}
1540		}
1541
1542		prUnit, err := repo.GetUnit(unit.TypePullRequests)
1543		if err != nil {
1544			ctx.ServerError("GetUnit", err)
1545			return
1546		}
1547		prConfig := prUnit.PullRequestsConfig()
1548
1549		// Check correct values and select default
1550		if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
1551			!prConfig.IsMergeStyleAllowed(ms) {
1552			defaultMergeStyle := prConfig.GetDefaultMergeStyle()
1553			if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
1554				ctx.Data["MergeStyle"] = defaultMergeStyle
1555			} else if prConfig.AllowMerge {
1556				ctx.Data["MergeStyle"] = repo_model.MergeStyleMerge
1557			} else if prConfig.AllowRebase {
1558				ctx.Data["MergeStyle"] = repo_model.MergeStyleRebase
1559			} else if prConfig.AllowRebaseMerge {
1560				ctx.Data["MergeStyle"] = repo_model.MergeStyleRebaseMerge
1561			} else if prConfig.AllowSquash {
1562				ctx.Data["MergeStyle"] = repo_model.MergeStyleSquash
1563			} else if prConfig.AllowManualMerge {
1564				ctx.Data["MergeStyle"] = repo_model.MergeStyleManuallyMerged
1565			} else {
1566				ctx.Data["MergeStyle"] = ""
1567			}
1568		}
1569		if err = pull.LoadProtectedBranch(); err != nil {
1570			ctx.ServerError("LoadProtectedBranch", err)
1571			return
1572		}
1573		ctx.Data["ShowMergeInstructions"] = true
1574		if pull.ProtectedBranch != nil {
1575			var showMergeInstructions bool
1576			if ctx.User != nil {
1577				showMergeInstructions = pull.ProtectedBranch.CanUserPush(ctx.User.ID)
1578			}
1579			cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
1580			ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
1581			ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
1582			ctx.Data["IsBlockedByOfficialReviewRequests"] = pull.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pull)
1583			ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
1584			ctx.Data["GrantedApprovals"] = cnt
1585			ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
1586			ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
1587			ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
1588			ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
1589			ctx.Data["ShowMergeInstructions"] = showMergeInstructions
1590		}
1591		ctx.Data["WillSign"] = false
1592		if ctx.User != nil {
1593			sign, key, _, err := asymkey_service.SignMerge(pull, ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
1594			ctx.Data["WillSign"] = sign
1595			ctx.Data["SigningKey"] = key
1596			if err != nil {
1597				if asymkey_service.IsErrWontSign(err) {
1598					ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason
1599				} else {
1600					ctx.Data["WontSignReason"] = "error"
1601					log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
1602				}
1603			}
1604		} else {
1605			ctx.Data["WontSignReason"] = "not_signed_in"
1606		}
1607
1608		isPullBranchDeletable := canDelete &&
1609			pull.HeadRepo != nil &&
1610			git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
1611			(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
1612
1613		if isPullBranchDeletable && pull.HasMerged {
1614			exist, err := models.HasUnmergedPullRequestsByHeadInfo(pull.HeadRepoID, pull.HeadBranch)
1615			if err != nil {
1616				ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
1617				return
1618			}
1619
1620			isPullBranchDeletable = !exist
1621		}
1622		ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable
1623
1624		stillCanManualMerge := func() bool {
1625			if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
1626				return false
1627			}
1628			if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() {
1629				return false
1630			}
1631			if (ctx.User.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge {
1632				return true
1633			}
1634
1635			return false
1636		}
1637
1638		ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
1639	}
1640
1641	// Get Dependencies
1642	ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
1643	if err != nil {
1644		ctx.ServerError("BlockedByDependencies", err)
1645		return
1646	}
1647	ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
1648	if err != nil {
1649		ctx.ServerError("BlockingDependencies", err)
1650		return
1651	}
1652
1653	ctx.Data["Participants"] = participants
1654	ctx.Data["NumParticipants"] = len(participants)
1655	ctx.Data["Issue"] = issue
1656	ctx.Data["Reference"] = issue.Ref
1657	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
1658	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
1659	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
1660	ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
1661	ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
1662	ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
1663	ctx.Data["RefEndName"] = git.RefEndName(issue.Ref)
1664	ctx.HTML(http.StatusOK, tplIssueView)
1665}
1666
1667// GetActionIssue will return the issue which is used in the context.
1668func GetActionIssue(ctx *context.Context) *models.Issue {
1669	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
1670	if err != nil {
1671		ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
1672		return nil
1673	}
1674	issue.Repo = ctx.Repo.Repository
1675	checkIssueRights(ctx, issue)
1676	if ctx.Written() {
1677		return nil
1678	}
1679	if err = issue.LoadAttributes(); err != nil {
1680		ctx.ServerError("LoadAttributes", nil)
1681		return nil
1682	}
1683	return issue
1684}
1685
1686func checkIssueRights(ctx *context.Context, issue *models.Issue) {
1687	if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
1688		!issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
1689		ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
1690	}
1691}
1692
1693func getActionIssues(ctx *context.Context) []*models.Issue {
1694	commaSeparatedIssueIDs := ctx.FormString("issue_ids")
1695	if len(commaSeparatedIssueIDs) == 0 {
1696		return nil
1697	}
1698	issueIDs := make([]int64, 0, 10)
1699	for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
1700		issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
1701		if err != nil {
1702			ctx.ServerError("ParseInt", err)
1703			return nil
1704		}
1705		issueIDs = append(issueIDs, issueID)
1706	}
1707	issues, err := models.GetIssuesByIDs(issueIDs)
1708	if err != nil {
1709		ctx.ServerError("GetIssuesByIDs", err)
1710		return nil
1711	}
1712	// Check access rights for all issues
1713	issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
1714	prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
1715	for _, issue := range issues {
1716		if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
1717			ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
1718			return nil
1719		}
1720		if err = issue.LoadAttributes(); err != nil {
1721			ctx.ServerError("LoadAttributes", err)
1722			return nil
1723		}
1724	}
1725	return issues
1726}
1727
1728// UpdateIssueTitle change issue's title
1729func UpdateIssueTitle(ctx *context.Context) {
1730	issue := GetActionIssue(ctx)
1731	if ctx.Written() {
1732		return
1733	}
1734
1735	if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
1736		ctx.Error(http.StatusForbidden)
1737		return
1738	}
1739
1740	title := ctx.FormTrim("title")
1741	if len(title) == 0 {
1742		ctx.Error(http.StatusNoContent)
1743		return
1744	}
1745
1746	if err := issue_service.ChangeTitle(issue, ctx.User, title); err != nil {
1747		ctx.ServerError("ChangeTitle", err)
1748		return
1749	}
1750
1751	ctx.JSON(http.StatusOK, map[string]interface{}{
1752		"title": issue.Title,
1753	})
1754}
1755
1756// UpdateIssueRef change issue's ref (branch)
1757func UpdateIssueRef(ctx *context.Context) {
1758	issue := GetActionIssue(ctx)
1759	if ctx.Written() {
1760		return
1761	}
1762
1763	if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
1764		ctx.Error(http.StatusForbidden)
1765		return
1766	}
1767
1768	ref := ctx.FormTrim("ref")
1769
1770	if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil {
1771		ctx.ServerError("ChangeRef", err)
1772		return
1773	}
1774
1775	ctx.JSON(http.StatusOK, map[string]interface{}{
1776		"ref": ref,
1777	})
1778}
1779
1780// UpdateIssueContent change issue's content
1781func UpdateIssueContent(ctx *context.Context) {
1782	issue := GetActionIssue(ctx)
1783	if ctx.Written() {
1784		return
1785	}
1786
1787	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
1788		ctx.Error(http.StatusForbidden)
1789		return
1790	}
1791
1792	if err := issue_service.ChangeContent(issue, ctx.User, ctx.Req.FormValue("content")); err != nil {
1793		ctx.ServerError("ChangeContent", err)
1794		return
1795	}
1796
1797	// when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
1798	if !ctx.FormBool("ignore_attachments") {
1799		if err := updateAttachments(issue, ctx.FormStrings("files[]")); err != nil {
1800			ctx.ServerError("UpdateAttachments", err)
1801			return
1802		}
1803	}
1804
1805	content, err := markdown.RenderString(&markup.RenderContext{
1806		URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
1807		Metas:     ctx.Repo.Repository.ComposeMetas(),
1808		GitRepo:   ctx.Repo.GitRepo,
1809		Ctx:       ctx,
1810	}, issue.Content)
1811	if err != nil {
1812		ctx.ServerError("RenderString", err)
1813		return
1814	}
1815
1816	ctx.JSON(http.StatusOK, map[string]interface{}{
1817		"content":     content,
1818		"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
1819	})
1820}
1821
1822// UpdateIssueMilestone change issue's milestone
1823func UpdateIssueMilestone(ctx *context.Context) {
1824	issues := getActionIssues(ctx)
1825	if ctx.Written() {
1826		return
1827	}
1828
1829	milestoneID := ctx.FormInt64("id")
1830	for _, issue := range issues {
1831		oldMilestoneID := issue.MilestoneID
1832		if oldMilestoneID == milestoneID {
1833			continue
1834		}
1835		issue.MilestoneID = milestoneID
1836		if err := issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
1837			ctx.ServerError("ChangeMilestoneAssign", err)
1838			return
1839		}
1840	}
1841
1842	ctx.JSON(http.StatusOK, map[string]interface{}{
1843		"ok": true,
1844	})
1845}
1846
1847// UpdateIssueAssignee change issue's or pull's assignee
1848func UpdateIssueAssignee(ctx *context.Context) {
1849	issues := getActionIssues(ctx)
1850	if ctx.Written() {
1851		return
1852	}
1853
1854	assigneeID := ctx.FormInt64("id")
1855	action := ctx.FormString("action")
1856
1857	for _, issue := range issues {
1858		switch action {
1859		case "clear":
1860			if err := issue_service.DeleteNotPassedAssignee(issue, ctx.User, []*user_model.User{}); err != nil {
1861				ctx.ServerError("ClearAssignees", err)
1862				return
1863			}
1864		default:
1865			assignee, err := user_model.GetUserByID(assigneeID)
1866			if err != nil {
1867				ctx.ServerError("GetUserByID", err)
1868				return
1869			}
1870
1871			valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
1872			if err != nil {
1873				ctx.ServerError("canBeAssigned", err)
1874				return
1875			}
1876			if !valid {
1877				ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
1878				return
1879			}
1880
1881			_, _, err = issue_service.ToggleAssignee(issue, ctx.User, assigneeID)
1882			if err != nil {
1883				ctx.ServerError("ToggleAssignee", err)
1884				return
1885			}
1886		}
1887	}
1888	ctx.JSON(http.StatusOK, map[string]interface{}{
1889		"ok": true,
1890	})
1891}
1892
1893// UpdatePullReviewRequest add or remove review request
1894func UpdatePullReviewRequest(ctx *context.Context) {
1895	issues := getActionIssues(ctx)
1896	if ctx.Written() {
1897		return
1898	}
1899
1900	reviewID := ctx.FormInt64("id")
1901	action := ctx.FormString("action")
1902
1903	// TODO: Not support 'clear' now
1904	if action != "attach" && action != "detach" {
1905		ctx.Status(403)
1906		return
1907	}
1908
1909	for _, issue := range issues {
1910		if err := issue.LoadRepo(); err != nil {
1911			ctx.ServerError("issue.LoadRepo", err)
1912			return
1913		}
1914
1915		if !issue.IsPull {
1916			log.Warn(
1917				"UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d",
1918				issue.Repo, issue.Index,
1919			)
1920			ctx.Status(403)
1921			return
1922		}
1923		if reviewID < 0 {
1924			// negative reviewIDs represent team requests
1925			if err := issue.Repo.GetOwner(db.DefaultContext); err != nil {
1926				ctx.ServerError("issue.Repo.GetOwner", err)
1927				return
1928			}
1929
1930			if !issue.Repo.Owner.IsOrganization() {
1931				log.Warn(
1932					"UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]",
1933					issue.Repo.FullName(), issue.Index, issue.Repo.ID,
1934				)
1935				ctx.Status(403)
1936				return
1937			}
1938
1939			team, err := models.GetTeamByID(-reviewID)
1940			if err != nil {
1941				ctx.ServerError("models.GetTeamByID", err)
1942				return
1943			}
1944
1945			if team.OrgID != issue.Repo.OwnerID {
1946				log.Warn(
1947					"UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]",
1948					team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID)
1949				ctx.Status(403)
1950				return
1951			}
1952
1953			err = issue_service.IsValidTeamReviewRequest(team, ctx.User, action == "attach", issue)
1954			if err != nil {
1955				if models.IsErrNotValidReviewRequest(err) {
1956					log.Warn(
1957						"UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
1958						team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
1959						err,
1960					)
1961					ctx.Status(403)
1962					return
1963				}
1964				ctx.ServerError("IsValidTeamReviewRequest", err)
1965				return
1966			}
1967
1968			_, err = issue_service.TeamReviewRequest(issue, ctx.User, team, action == "attach")
1969			if err != nil {
1970				ctx.ServerError("TeamReviewRequest", err)
1971				return
1972			}
1973			continue
1974		}
1975
1976		reviewer, err := user_model.GetUserByID(reviewID)
1977		if err != nil {
1978			if user_model.IsErrUserNotExist(err) {
1979				log.Warn(
1980					"UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v",
1981					reviewID, issue.Repo, issue.Index,
1982					err,
1983				)
1984				ctx.Status(403)
1985				return
1986			}
1987			ctx.ServerError("GetUserByID", err)
1988			return
1989		}
1990
1991		err = issue_service.IsValidReviewRequest(reviewer, ctx.User, action == "attach", issue, nil)
1992		if err != nil {
1993			if models.IsErrNotValidReviewRequest(err) {
1994				log.Warn(
1995					"UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
1996					reviewer, issue.Repo, issue.Index,
1997					err,
1998				)
1999				ctx.Status(403)
2000				return
2001			}
2002			ctx.ServerError("isValidReviewRequest", err)
2003			return
2004		}
2005
2006		_, err = issue_service.ReviewRequest(issue, ctx.User, reviewer, action == "attach")
2007		if err != nil {
2008			ctx.ServerError("ReviewRequest", err)
2009			return
2010		}
2011	}
2012
2013	ctx.JSON(http.StatusOK, map[string]interface{}{
2014		"ok": true,
2015	})
2016}
2017
2018// UpdateIssueStatus change issue's status
2019func UpdateIssueStatus(ctx *context.Context) {
2020	issues := getActionIssues(ctx)
2021	if ctx.Written() {
2022		return
2023	}
2024
2025	var isClosed bool
2026	switch action := ctx.FormString("action"); action {
2027	case "open":
2028		isClosed = false
2029	case "close":
2030		isClosed = true
2031	default:
2032		log.Warn("Unrecognized action: %s", action)
2033	}
2034
2035	if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
2036		ctx.ServerError("LoadRepositories", err)
2037		return
2038	}
2039	for _, issue := range issues {
2040		if issue.IsClosed != isClosed {
2041			if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
2042				if models.IsErrDependenciesLeft(err) {
2043					ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
2044						"error": "cannot close this issue because it still has open dependencies",
2045					})
2046					return
2047				}
2048				ctx.ServerError("ChangeStatus", err)
2049				return
2050			}
2051		}
2052	}
2053	ctx.JSON(http.StatusOK, map[string]interface{}{
2054		"ok": true,
2055	})
2056}
2057
2058// NewComment create a comment for issue
2059func NewComment(ctx *context.Context) {
2060	form := web.GetForm(ctx).(*forms.CreateCommentForm)
2061	issue := GetActionIssue(ctx)
2062	if ctx.Written() {
2063		return
2064	}
2065
2066	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
2067		if log.IsTrace() {
2068			if ctx.IsSigned {
2069				issueType := "issues"
2070				if issue.IsPull {
2071					issueType = "pulls"
2072				}
2073				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
2074					"User in Repo has Permissions: %-+v",
2075					ctx.User,
2076					log.NewColoredIDValue(issue.PosterID),
2077					issueType,
2078					ctx.Repo.Repository,
2079					ctx.Repo.Permission)
2080			} else {
2081				log.Trace("Permission Denied: Not logged in")
2082			}
2083		}
2084
2085		ctx.Error(http.StatusForbidden)
2086		return
2087	}
2088
2089	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.User.IsAdmin {
2090		ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
2091		ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
2092		return
2093	}
2094
2095	var attachments []string
2096	if setting.Attachment.Enabled {
2097		attachments = form.Files
2098	}
2099
2100	if ctx.HasError() {
2101		ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
2102		ctx.Redirect(issue.HTMLURL())
2103		return
2104	}
2105
2106	var comment *models.Comment
2107	defer func() {
2108		// Check if issue admin/poster changes the status of issue.
2109		if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
2110			(form.Status == "reopen" || form.Status == "close") &&
2111			!(issue.IsPull && issue.PullRequest.HasMerged) {
2112
2113			// Duplication and conflict check should apply to reopen pull request.
2114			var pr *models.PullRequest
2115
2116			if form.Status == "reopen" && issue.IsPull {
2117				pull := issue.PullRequest
2118				var err error
2119				pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
2120				if err != nil {
2121					if !models.IsErrPullRequestNotExist(err) {
2122						ctx.ServerError("GetUnmergedPullRequest", err)
2123						return
2124					}
2125				}
2126
2127				// Regenerate patch and test conflict.
2128				if pr == nil {
2129					issue.PullRequest.HeadCommitID = ""
2130					pull_service.AddToTaskQueue(issue.PullRequest)
2131				}
2132			}
2133
2134			if pr != nil {
2135				ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
2136			} else {
2137				isClosed := form.Status == "close"
2138				if err := issue_service.ChangeStatus(issue, ctx.User, isClosed); err != nil {
2139					log.Error("ChangeStatus: %v", err)
2140
2141					if models.IsErrDependenciesLeft(err) {
2142						if issue.IsPull {
2143							ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
2144							ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
2145						} else {
2146							ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
2147							ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
2148						}
2149						return
2150					}
2151				} else {
2152					if err := stopTimerIfAvailable(ctx.User, issue); err != nil {
2153						ctx.ServerError("CreateOrStopIssueStopwatch", err)
2154						return
2155					}
2156
2157					log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
2158				}
2159			}
2160		}
2161
2162		// Redirect to comment hashtag if there is any actual content.
2163		typeName := "issues"
2164		if issue.IsPull {
2165			typeName = "pulls"
2166		}
2167		if comment != nil {
2168			ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
2169		} else {
2170			ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
2171		}
2172	}()
2173
2174	// Fix #321: Allow empty comments, as long as we have attachments.
2175	if len(form.Content) == 0 && len(attachments) == 0 {
2176		return
2177	}
2178
2179	comment, err := comment_service.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
2180	if err != nil {
2181		ctx.ServerError("CreateIssueComment", err)
2182		return
2183	}
2184
2185	log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
2186}
2187
2188// UpdateCommentContent change comment of issue's content
2189func UpdateCommentContent(ctx *context.Context) {
2190	comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
2191	if err != nil {
2192		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
2193		return
2194	}
2195
2196	if err := comment.LoadIssue(); err != nil {
2197		ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
2198		return
2199	}
2200
2201	if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
2202		ctx.Error(http.StatusForbidden)
2203		return
2204	}
2205
2206	if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode {
2207		ctx.Error(http.StatusNoContent)
2208		return
2209	}
2210
2211	oldContent := comment.Content
2212	comment.Content = ctx.FormString("content")
2213	if len(comment.Content) == 0 {
2214		ctx.JSON(http.StatusOK, map[string]interface{}{
2215			"content": "",
2216		})
2217		return
2218	}
2219	if err = comment_service.UpdateComment(comment, ctx.User, oldContent); err != nil {
2220		ctx.ServerError("UpdateComment", err)
2221		return
2222	}
2223
2224	if err := comment.LoadAttachments(); err != nil {
2225		ctx.ServerError("LoadAttachments", err)
2226		return
2227	}
2228
2229	// when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
2230	if !ctx.FormBool("ignore_attachments") {
2231		if err := updateAttachments(comment, ctx.FormStrings("files[]")); err != nil {
2232			ctx.ServerError("UpdateAttachments", err)
2233			return
2234		}
2235	}
2236
2237	content, err := markdown.RenderString(&markup.RenderContext{
2238		URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
2239		Metas:     ctx.Repo.Repository.ComposeMetas(),
2240		GitRepo:   ctx.Repo.GitRepo,
2241		Ctx:       ctx,
2242	}, comment.Content)
2243	if err != nil {
2244		ctx.ServerError("RenderString", err)
2245		return
2246	}
2247
2248	ctx.JSON(http.StatusOK, map[string]interface{}{
2249		"content":     content,
2250		"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
2251	})
2252}
2253
2254// DeleteComment delete comment of issue
2255func DeleteComment(ctx *context.Context) {
2256	comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
2257	if err != nil {
2258		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
2259		return
2260	}
2261
2262	if err := comment.LoadIssue(); err != nil {
2263		ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
2264		return
2265	}
2266
2267	if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
2268		ctx.Error(http.StatusForbidden)
2269		return
2270	} else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
2271		ctx.Error(http.StatusNoContent)
2272		return
2273	}
2274
2275	if err = comment_service.DeleteComment(ctx.User, comment); err != nil {
2276		ctx.ServerError("DeleteCommentByID", err)
2277		return
2278	}
2279
2280	ctx.Status(200)
2281}
2282
2283// ChangeIssueReaction create a reaction for issue
2284func ChangeIssueReaction(ctx *context.Context) {
2285	form := web.GetForm(ctx).(*forms.ReactionForm)
2286	issue := GetActionIssue(ctx)
2287	if ctx.Written() {
2288		return
2289	}
2290
2291	if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
2292		if log.IsTrace() {
2293			if ctx.IsSigned {
2294				issueType := "issues"
2295				if issue.IsPull {
2296					issueType = "pulls"
2297				}
2298				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
2299					"User in Repo has Permissions: %-+v",
2300					ctx.User,
2301					log.NewColoredIDValue(issue.PosterID),
2302					issueType,
2303					ctx.Repo.Repository,
2304					ctx.Repo.Permission)
2305			} else {
2306				log.Trace("Permission Denied: Not logged in")
2307			}
2308		}
2309
2310		ctx.Error(http.StatusForbidden)
2311		return
2312	}
2313
2314	if ctx.HasError() {
2315		ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
2316		return
2317	}
2318
2319	switch ctx.Params(":action") {
2320	case "react":
2321		reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
2322		if err != nil {
2323			if models.IsErrForbiddenIssueReaction(err) {
2324				ctx.ServerError("ChangeIssueReaction", err)
2325				return
2326			}
2327			log.Info("CreateIssueReaction: %s", err)
2328			break
2329		}
2330		// Reload new reactions
2331		issue.Reactions = nil
2332		if err = issue.LoadAttributes(); err != nil {
2333			log.Info("issue.LoadAttributes: %s", err)
2334			break
2335		}
2336
2337		log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
2338	case "unreact":
2339		if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
2340			ctx.ServerError("DeleteIssueReaction", err)
2341			return
2342		}
2343
2344		// Reload new reactions
2345		issue.Reactions = nil
2346		if err := issue.LoadAttributes(); err != nil {
2347			log.Info("issue.LoadAttributes: %s", err)
2348			break
2349		}
2350
2351		log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
2352	default:
2353		ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
2354		return
2355	}
2356
2357	if len(issue.Reactions) == 0 {
2358		ctx.JSON(http.StatusOK, map[string]interface{}{
2359			"empty": true,
2360			"html":  "",
2361		})
2362		return
2363	}
2364
2365	html, err := ctx.RenderToString(tplReactions, map[string]interface{}{
2366		"ctx":       ctx.Data,
2367		"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
2368		"Reactions": issue.Reactions.GroupByType(),
2369	})
2370	if err != nil {
2371		ctx.ServerError("ChangeIssueReaction.HTMLString", err)
2372		return
2373	}
2374	ctx.JSON(http.StatusOK, map[string]interface{}{
2375		"html": html,
2376	})
2377}
2378
2379// ChangeCommentReaction create a reaction for comment
2380func ChangeCommentReaction(ctx *context.Context) {
2381	form := web.GetForm(ctx).(*forms.ReactionForm)
2382	comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
2383	if err != nil {
2384		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
2385		return
2386	}
2387
2388	if err := comment.LoadIssue(); err != nil {
2389		ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
2390		return
2391	}
2392
2393	if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
2394		if log.IsTrace() {
2395			if ctx.IsSigned {
2396				issueType := "issues"
2397				if comment.Issue.IsPull {
2398					issueType = "pulls"
2399				}
2400				log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
2401					"User in Repo has Permissions: %-+v",
2402					ctx.User,
2403					log.NewColoredIDValue(comment.Issue.PosterID),
2404					issueType,
2405					ctx.Repo.Repository,
2406					ctx.Repo.Permission)
2407			} else {
2408				log.Trace("Permission Denied: Not logged in")
2409			}
2410		}
2411
2412		ctx.Error(http.StatusForbidden)
2413		return
2414	}
2415
2416	if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode && comment.Type != models.CommentTypeReview {
2417		ctx.Error(http.StatusNoContent)
2418		return
2419	}
2420
2421	switch ctx.Params(":action") {
2422	case "react":
2423		reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
2424		if err != nil {
2425			if models.IsErrForbiddenIssueReaction(err) {
2426				ctx.ServerError("ChangeIssueReaction", err)
2427				return
2428			}
2429			log.Info("CreateCommentReaction: %s", err)
2430			break
2431		}
2432		// Reload new reactions
2433		comment.Reactions = nil
2434		if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
2435			log.Info("comment.LoadReactions: %s", err)
2436			break
2437		}
2438
2439		log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
2440	case "unreact":
2441		if err := models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Content); err != nil {
2442			ctx.ServerError("DeleteCommentReaction", err)
2443			return
2444		}
2445
2446		// Reload new reactions
2447		comment.Reactions = nil
2448		if err = comment.LoadReactions(ctx.Repo.Repository); err != nil {
2449			log.Info("comment.LoadReactions: %s", err)
2450			break
2451		}
2452
2453		log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
2454	default:
2455		ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
2456		return
2457	}
2458
2459	if len(comment.Reactions) == 0 {
2460		ctx.JSON(http.StatusOK, map[string]interface{}{
2461			"empty": true,
2462			"html":  "",
2463		})
2464		return
2465	}
2466
2467	html, err := ctx.RenderToString(tplReactions, map[string]interface{}{
2468		"ctx":       ctx.Data,
2469		"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
2470		"Reactions": comment.Reactions.GroupByType(),
2471	})
2472	if err != nil {
2473		ctx.ServerError("ChangeCommentReaction.HTMLString", err)
2474		return
2475	}
2476	ctx.JSON(http.StatusOK, map[string]interface{}{
2477		"html": html,
2478	})
2479}
2480
2481func addParticipant(poster *user_model.User, participants []*user_model.User) []*user_model.User {
2482	for _, part := range participants {
2483		if poster.ID == part.ID {
2484			return participants
2485		}
2486	}
2487	return append(participants, poster)
2488}
2489
2490func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
2491	// Remove comments that the user has no permissions to see
2492	for i := 0; i < len(issue.Comments); {
2493		c := issue.Comments[i]
2494		if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
2495			var err error
2496			// Set RefRepo for description in template
2497			c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID)
2498			if err != nil {
2499				return err
2500			}
2501			perm, err := models.GetUserRepoPermission(c.RefRepo, ctx.User)
2502			if err != nil {
2503				return err
2504			}
2505			if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
2506				issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
2507				continue
2508			}
2509		}
2510		i++
2511	}
2512	return nil
2513}
2514
2515// GetIssueAttachments returns attachments for the issue
2516func GetIssueAttachments(ctx *context.Context) {
2517	issue := GetActionIssue(ctx)
2518	var attachments = make([]*api.Attachment, len(issue.Attachments))
2519	for i := 0; i < len(issue.Attachments); i++ {
2520		attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
2521	}
2522	ctx.JSON(http.StatusOK, attachments)
2523}
2524
2525// GetCommentAttachments returns attachments for the comment
2526func GetCommentAttachments(ctx *context.Context) {
2527	comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
2528	if err != nil {
2529		ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
2530		return
2531	}
2532	var attachments = make([]*api.Attachment, 0)
2533	if comment.Type == models.CommentTypeComment {
2534		if err := comment.LoadAttachments(); err != nil {
2535			ctx.ServerError("LoadAttachments", err)
2536			return
2537		}
2538		for i := 0; i < len(comment.Attachments); i++ {
2539			attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
2540		}
2541	}
2542	ctx.JSON(http.StatusOK, attachments)
2543}
2544
2545func updateAttachments(item interface{}, files []string) error {
2546	var attachments []*repo_model.Attachment
2547	switch content := item.(type) {
2548	case *models.Issue:
2549		attachments = content.Attachments
2550	case *models.Comment:
2551		attachments = content.Attachments
2552	default:
2553		return fmt.Errorf("Unknown Type: %T", content)
2554	}
2555	for i := 0; i < len(attachments); i++ {
2556		if util.IsStringInSlice(attachments[i].UUID, files) {
2557			continue
2558		}
2559		if err := repo_model.DeleteAttachment(attachments[i], true); err != nil {
2560			return err
2561		}
2562	}
2563	var err error
2564	if len(files) > 0 {
2565		switch content := item.(type) {
2566		case *models.Issue:
2567			err = content.UpdateAttachments(files)
2568		case *models.Comment:
2569			err = content.UpdateAttachments(files)
2570		default:
2571			return fmt.Errorf("Unknown Type: %T", content)
2572		}
2573		if err != nil {
2574			return err
2575		}
2576	}
2577	switch content := item.(type) {
2578	case *models.Issue:
2579		content.Attachments, err = repo_model.GetAttachmentsByIssueID(content.ID)
2580	case *models.Comment:
2581		content.Attachments, err = repo_model.GetAttachmentsByCommentID(content.ID)
2582	default:
2583		return fmt.Errorf("Unknown Type: %T", content)
2584	}
2585	return err
2586}
2587
2588func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
2589	attachHTML, err := ctx.RenderToString(tplAttachment, map[string]interface{}{
2590		"ctx":         ctx.Data,
2591		"Attachments": attachments,
2592		"Content":     content,
2593	})
2594	if err != nil {
2595		ctx.ServerError("attachmentsHTML.HTMLString", err)
2596		return ""
2597	}
2598	return attachHTML
2599}
2600
2601// combineLabelComments combine the nearby label comments as one.
2602func combineLabelComments(issue *models.Issue) {
2603	var prev, cur *models.Comment
2604	for i := 0; i < len(issue.Comments); i++ {
2605		cur = issue.Comments[i]
2606		if i > 0 {
2607			prev = issue.Comments[i-1]
2608		}
2609		if i == 0 || cur.Type != models.CommentTypeLabel ||
2610			(prev != nil && prev.PosterID != cur.PosterID) ||
2611			(prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
2612			if cur.Type == models.CommentTypeLabel && cur.Label != nil {
2613				if cur.Content != "1" {
2614					cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
2615				} else {
2616					cur.AddedLabels = append(cur.AddedLabels, cur.Label)
2617				}
2618			}
2619			continue
2620		}
2621
2622		if cur.Label != nil { // now cur MUST be label comment
2623			if prev.Type == models.CommentTypeLabel { // we can combine them only prev is a label comment
2624				if cur.Content != "1" {
2625					// remove labels from the AddedLabels list if the label that was removed is already
2626					// in this list, and if it's not in this list, add the label to RemovedLabels
2627					addedAndRemoved := false
2628					for i, label := range prev.AddedLabels {
2629						if cur.Label.ID == label.ID {
2630							prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...)
2631							addedAndRemoved = true
2632							break
2633						}
2634					}
2635					if !addedAndRemoved {
2636						prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
2637					}
2638				} else {
2639					// remove labels from the RemovedLabels list if the label that was added is already
2640					// in this list, and if it's not in this list, add the label to AddedLabels
2641					removedAndAdded := false
2642					for i, label := range prev.RemovedLabels {
2643						if cur.Label.ID == label.ID {
2644							prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...)
2645							removedAndAdded = true
2646							break
2647						}
2648					}
2649					if !removedAndAdded {
2650						prev.AddedLabels = append(prev.AddedLabels, cur.Label)
2651					}
2652				}
2653				prev.CreatedUnix = cur.CreatedUnix
2654				// remove the current comment since it has been combined to prev comment
2655				issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
2656				i--
2657			} else { // if prev is not a label comment, start a new group
2658				if cur.Content != "1" {
2659					cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
2660				} else {
2661					cur.AddedLabels = append(cur.AddedLabels, cur.Label)
2662				}
2663			}
2664		}
2665	}
2666}
2667
2668// get all teams that current user can mention
2669func handleTeamMentions(ctx *context.Context) {
2670	if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
2671		return
2672	}
2673
2674	var isAdmin bool
2675	var err error
2676	var teams []*models.Team
2677	var org = models.OrgFromUser(ctx.Repo.Owner)
2678	// Admin has super access.
2679	if ctx.User.IsAdmin {
2680		isAdmin = true
2681	} else {
2682		isAdmin, err = org.IsOwnedBy(ctx.User.ID)
2683		if err != nil {
2684			ctx.ServerError("IsOwnedBy", err)
2685			return
2686		}
2687	}
2688
2689	if isAdmin {
2690		teams, err = org.LoadTeams()
2691		if err != nil {
2692			ctx.ServerError("LoadTeams", err)
2693			return
2694		}
2695	} else {
2696		teams, err = org.GetUserTeams(ctx.User.ID)
2697		if err != nil {
2698			ctx.ServerError("GetUserTeams", err)
2699			return
2700		}
2701	}
2702
2703	ctx.Data["MentionableTeams"] = teams
2704	ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
2705	ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink()
2706}
2707