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