Files
Gitea/services/issue/comments.go
T
178d33ed4d fix: add missing access checks and size limits (#39942)
Several handlers and parsers skipped checks that their siblings already
perform.

- Reject issue, comment, label, milestone and release mutations in
archived repositories via the API with one guard per route group,
matching the web UI
- Hide timeline references and dependencies pointing at issues the doer
cannot read, in the web UI and the API, including pull requests of the
same repository without pull request access
- Reject workflow runs with more than `MaxJobNumPerRun` jobs, including
inline reusable workflow jobs, and show rejected pushes as invalid
workflow runs
- Bound how much of a Helm chart archive is inflated while looking for
`Chart.yaml`

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-27 21:40:42 +01:00

252 lines
7.3 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issue
import (
"context"
"errors"
"fmt"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/git"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
git_service "gitea.dev/services/git"
notify_service "gitea.dev/services/notify"
"xorm.io/builder"
)
// CreateRefComment creates a commit reference comment to issue.
func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, commitSHA string) error {
if len(commitSHA) == 0 {
return errors.New("cannot create reference with empty commit SHA")
}
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
if !access_model.IsUserRepoAdmin(ctx, repo, doer) {
return user_model.ErrBlockedUser
}
}
// Check if same reference from same commit has already existed.
has, err := db.Exist[issues_model.Comment](ctx, builder.Eq{
"`type`": issues_model.CommentTypeCommitRef,
"issue_id": issue.ID,
"commit_sha": commitSHA,
})
if err != nil {
return fmt.Errorf("check reference comment: %w", err)
} else if has {
return nil
}
_, err = issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeCommitRef,
Doer: doer,
Repo: repo,
Issue: issue,
CommitSHA: commitSHA,
Content: content,
})
return err
}
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
if !access_model.IsUserRepoAdmin(ctx, repo, doer) {
return nil, user_model.ErrBlockedUser
}
}
comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeComment,
Doer: doer,
Repo: repo,
Issue: issue,
Content: content,
Attachments: attachments,
})
if err != nil {
return nil, err
}
mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
if err != nil {
return nil, err
}
// reload issue to ensure it has the latest data, especially the number of comments
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
return nil, err
}
notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
return comment, nil
}
// UpdateComment updates information of comment.
func UpdateComment(ctx context.Context, c *issues_model.Comment, contentVersion int, doer *user_model.User, oldContent string) error {
if err := c.LoadIssue(ctx); err != nil {
return err
}
if err := c.Issue.LoadRepo(ctx); err != nil {
return err
}
if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
if !access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer) {
return user_model.ErrBlockedUser
}
}
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
if needsContentHistory {
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
if err != nil {
return err
}
if !hasContentHistory {
if err = issues_model.SaveIssueContentHistory(ctx, c.PosterID, c.IssueID, c.ID,
c.CreatedUnix, oldContent, true); err != nil {
return err
}
}
}
if err := issues_model.UpdateComment(ctx, c, contentVersion, doer); err != nil {
return err
}
if needsContentHistory {
err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
if err != nil {
return err
}
}
notify_service.UpdateComment(ctx, doer, c, oldContent)
return nil
}
// DeleteComment deletes the comment
func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
return issues_model.DeleteComment(ctx, comment)
})
if err != nil {
return err
}
notify_service.DeleteComment(ctx, doer, comment)
return nil
}
// LoadCommentPushCommits Load push commits
func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) error {
if c.Content == "" || c.Commits != nil || c.Type != issues_model.CommentTypePullRequestPush {
return nil
}
var data issues_model.PushActionContent
if err := json.Unmarshal([]byte(c.Content), &data); err != nil {
log.Debug("Unmarshal: %v", err) // no need to show 500 error to end user when the JSON is broken
return nil
}
c.IsForcePush = data.IsForcePush
if c.IsForcePush {
if len(data.CommitIDs) != 2 {
return nil
}
c.OldCommit, c.NewCommit = data.CommitIDs[0], data.CommitIDs[1]
} else {
if err := c.LoadIssue(ctx); err != nil {
return err
}
if err := c.Issue.LoadRepo(ctx); err != nil {
return err
}
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, c.Issue.Repo)
if err != nil {
return err
}
defer closer.Close()
c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(ctx, data.CommitIDs), c.Issue.Repo, "") // no current ref sub path for PR commit list
if err != nil {
log.Debug("ConvertFromGitCommit: %v", err) // no need to show 500 error to end user when the commit does not exist
} else {
c.CommitsNum = int64(len(c.Commits))
}
}
return nil
}
// FilterReadableComments drops comments pointing at issues doer cannot read and loads RefRepo of the rest
func FilterReadableComments(ctx context.Context, doer *user_model.User, perm access_model.Permission, issue *issues_model.Issue, comments issues_model.CommentList) (issues_model.CommentList, error) {
type refTarget struct {
repo *repo_model.Repository
perm access_model.Permission
}
targets := cache.NewEphemeralCache()
targets.Put("ref-target", issue.RepoID, refTarget{issue.Repo, perm})
getRepoPerm := func(refRepoID int64) (refTarget, error) {
return cache.GetWithEphemeralCache(ctx, targets, "ref-target", refRepoID, func(ctx context.Context, repoID int64) (refTarget, error) {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return refTarget{}, err
}
repoPerm, err := access_model.GetDoerRepoPermission(ctx, repo, doer)
return refTarget{repo, repoPerm}, err
})
}
readable := comments[:0]
for _, c := range comments {
var refRepoID int64
var refIsPull bool
switch {
case issues_model.CommentTypeIsRef(c.Type):
refRepoID, refIsPull = c.RefRepoID, c.RefIsPull
case c.Type == issues_model.CommentTypeAddDependency || c.Type == issues_model.CommentTypeRemoveDependency:
if err := c.LoadDepIssueDetails(ctx); err != nil && !issues_model.IsErrIssueNotExist(err) {
return nil, err
}
if c.DependentIssue != nil {
refRepoID, refIsPull = c.DependentIssue.RepoID, c.DependentIssue.IsPull
}
}
if refRepoID != 0 {
target, err := getRepoPerm(refRepoID)
if err != nil {
return nil, err
}
if !target.perm.CanReadIssuesOrPulls(refIsPull) {
continue
}
c.RefRepo = target.repo
if c.DependentIssue != nil {
c.DependentIssue.Repo = target.repo
}
}
readable = append(readable, c)
}
return readable, nil
}