mirror of
https://github.com/go-gitea/gitea.git
synced 2024-09-30 03:26:01 -04:00
Merge branch 'main' into tsconfig
This commit is contained in:
commit
d0020df4fe
@ -25,7 +25,7 @@ It is designed to be compatible with [GitHub Actions workflow badge](https://doc
|
||||
You can use the following URL to get the badge:
|
||||
|
||||
```
|
||||
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
|
||||
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}/badge.svg?branch={branch}&event={event}
|
||||
```
|
||||
|
||||
- `{owner}`: The owner of the repository.
|
||||
|
@ -54,7 +54,6 @@ type FindTaskOptions struct {
|
||||
UpdatedBefore timeutil.TimeStamp
|
||||
StartedBefore timeutil.TimeStamp
|
||||
RunnerID int64
|
||||
IDOrderDesc bool
|
||||
}
|
||||
|
||||
func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||
@ -84,8 +83,5 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||
}
|
||||
|
||||
func (opts FindTaskOptions) ToOrders() string {
|
||||
if opts.IDOrderDesc {
|
||||
return "`id` DESC"
|
||||
}
|
||||
return ""
|
||||
return "`id` DESC"
|
||||
}
|
||||
|
@ -107,17 +107,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
|
||||
|
||||
func (opts FindBranchOptions) ToOrders() string {
|
||||
orderBy := opts.OrderBy
|
||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
|
||||
if orderBy != "" {
|
||||
orderBy += ", "
|
||||
}
|
||||
orderBy += "is_deleted ASC"
|
||||
}
|
||||
if orderBy == "" {
|
||||
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
||||
return "commit_time DESC, name ASC"
|
||||
orderBy = "commit_time DESC, name ASC"
|
||||
}
|
||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the beginning
|
||||
orderBy = "is_deleted ASC, " + orderBy
|
||||
}
|
||||
|
||||
return orderBy
|
||||
}
|
||||
|
||||
|
@ -27,23 +27,27 @@ func init() {
|
||||
|
||||
// LoadAssignees load assignees of this issue.
|
||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||
if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset maybe preexisting assignees
|
||||
issue.Assignees = []*user_model.User{}
|
||||
issue.Assignee = nil
|
||||
|
||||
err = db.GetEngine(ctx).Table("`user`").
|
||||
if err = db.GetEngine(ctx).Table("`user`").
|
||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||
Find(&issue.Assignees)
|
||||
if err != nil {
|
||||
Find(&issue.Assignees); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.isAssigneeLoaded = true
|
||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||
|
@ -16,25 +16,25 @@ import (
|
||||
// CommentList defines a list of comments
|
||||
type CommentList []*Comment
|
||||
|
||||
func (comments CommentList) getPosterIDs() []int64 {
|
||||
return container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||
return c.PosterID, c.PosterID > 0
|
||||
})
|
||||
}
|
||||
|
||||
// LoadPosters loads posters
|
||||
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterMaps, err := getPosters(ctx, comments.getPosterIDs())
|
||||
posterIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||
return c.PosterID, c.Poster == nil && c.PosterID > 0
|
||||
})
|
||||
|
||||
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||
if comment.Poster == nil {
|
||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -98,32 +98,35 @@ var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already
|
||||
|
||||
// Issue represents an issue or pull request of repository.
|
||||
type Issue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
Ref string
|
||||
PinOrder int `xorm:"DEFAULT 0"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
isLabelsLoaded bool `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
isMilestoneLoaded bool `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
isAssigneeLoaded bool `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
Ref string
|
||||
PinOrder int `xorm:"DEFAULT 0"`
|
||||
|
||||
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
|
||||
@ -131,11 +134,12 @@ type Issue struct {
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*user_model.User `xorm:"-"`
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
isAttachmentsLoaded bool `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*user_model.User `xorm:"-"`
|
||||
|
||||
// IsLocked limits commenting abilities to users on an issue
|
||||
// with write access
|
||||
@ -187,6 +191,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
|
||||
if issue.isAttachmentsLoaded || issue.Attachments != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isAttachmentsLoaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
||||
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
@ -287,11 +304,12 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
|
||||
|
||||
// LoadMilestone load milestone of this issue.
|
||||
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
||||
if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
||||
}
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -327,11 +345,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if issue.Attachments == nil {
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
if err = issue.LoadAttachments(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = issue.loadComments(ctx); err != nil {
|
||||
@ -350,6 +365,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
||||
return issue.loadReactions(ctx)
|
||||
}
|
||||
|
||||
func (issue *Issue) ResetAttributesLoaded() {
|
||||
issue.isLabelsLoaded = false
|
||||
issue.isMilestoneLoaded = false
|
||||
issue.isAttachmentsLoaded = false
|
||||
issue.isAssigneeLoaded = false
|
||||
}
|
||||
|
||||
// GetIsRead load the `IsRead` field of the issue
|
||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
||||
|
@ -111,6 +111,7 @@ func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m
|
||||
return err
|
||||
}
|
||||
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
|
||||
return err
|
||||
}
|
||||
|
||||
// reload all labels
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
||||
|
||||
// LoadLabels loads labels
|
||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
||||
if issue.Labels == nil && issue.ID != 0 {
|
||||
if !issue.isLabelsLoaded && issue.Labels == nil && issue.ID != 0 {
|
||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -72,29 +72,29 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
|
||||
return repo_model.ValuesRepository(repoMaps), nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getPosterIDs() []int64 {
|
||||
return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||
return issue.PosterID, true
|
||||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
||||
func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterMaps, err := getPosters(ctx, issues.getPosterIDs())
|
||||
posterIDs := container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||
return issue.PosterID, issue.Poster == nil && issue.PosterID > 0
|
||||
})
|
||||
|
||||
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||
if issue.Poster == nil {
|
||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
||||
func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
||||
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||
left := len(posterIDs)
|
||||
for left > 0 {
|
||||
@ -136,7 +136,7 @@ func (issues IssueList) getIssueIDs() []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
func (issues IssueList) LoadLabels(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -168,7 +168,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
err = rows.Scan(&labelIssue)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -177,7 +177,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
// When there are no rows left and we try to close it.
|
||||
// Since that is not relevant for us, we can safely ignore it.
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
@ -185,6 +185,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Labels = issueLabels[issue.ID]
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -195,7 +196,7 @@ func (issues IssueList) getMilestoneIDs() []int64 {
|
||||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||
func (issues IssueList) LoadMilestones(ctx context.Context) error {
|
||||
milestoneIDs := issues.getMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
@ -220,6 +221,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -263,7 +265,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
func (issues IssueList) LoadAssignees(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -310,6 +312,10 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Assignees = assignees[issue.ID]
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
issue.isAssigneeLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -413,6 +419,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Attachments = attachments[issue.ID]
|
||||
issue.isAttachmentsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -538,23 +545,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
|
||||
if err := issues.LoadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
|
||||
if err := issues.LoadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
|
||||
if err := issues.LoadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.LoadProjects(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadAssignees(ctx); err != nil {
|
||||
if err := issues.LoadAssignees(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
|
||||
}
|
||||
|
||||
|
@ -159,10 +159,11 @@ type PullRequest struct {
|
||||
|
||||
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
||||
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||
|
||||
HeadRepoID int64 `xorm:"INDEX"`
|
||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||
@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
|
||||
|
||||
// LoadRequestedReviewers loads the requested reviewers.
|
||||
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
if len(pr.RequestedReviewers) > 0 {
|
||||
if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
pr.isRequestedReviewersLoaded = true
|
||||
for _, review := range reviews {
|
||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@ -123,7 +125,7 @@ func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatu
|
||||
}
|
||||
|
||||
// PullRequests returns all pull requests for a base Repo by the given conditions
|
||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
|
||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (PullRequestList, int64, error) {
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
@ -153,50 +155,93 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
||||
// PullRequestList defines a list of pull requests
|
||||
type PullRequestList []*PullRequest
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||
repoIDs := make(container.Set[int64])
|
||||
for _, pr := range prs {
|
||||
if pr.BaseRepo == nil && pr.BaseRepoID > 0 {
|
||||
repoIDs.Add(pr.BaseRepoID)
|
||||
}
|
||||
if pr.HeadRepo == nil && pr.HeadRepoID > 0 {
|
||||
repoIDs.Add(pr.HeadRepoID)
|
||||
}
|
||||
}
|
||||
return repoIDs.Values()
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
issues := make([]*Issue, 0, len(issueIDs))
|
||||
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||
repoIDs := prs.getRepositoryIDs()
|
||||
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("id > 0").
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
set := make(map[int64]*Issue)
|
||||
for i := range issues {
|
||||
set[issues[i].ID] = issues[i]
|
||||
In("id", repoIDs).
|
||||
Find(&reposMap); err != nil {
|
||||
return fmt.Errorf("find repos: %w", err)
|
||||
}
|
||||
for _, pr := range prs {
|
||||
pr.Issue = set[pr.IssueID]
|
||||
/*
|
||||
Old code:
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
|
||||
It's worth panic because it's almost impossible to happen under normal use.
|
||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||
*/
|
||||
if pr.Issue == nil {
|
||||
return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||
if pr.BaseRepo == nil {
|
||||
pr.BaseRepo = reposMap[pr.BaseRepoID]
|
||||
}
|
||||
if pr.HeadRepo == nil {
|
||||
pr.HeadRepo = reposMap[pr.HeadRepoID]
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
pr.Issue.PullRequest = pr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if _, err := prs.LoadIssues(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
|
||||
if len(prs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return nil, fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
issueList := make(IssueList, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
if pr.Issue == nil {
|
||||
pr.Issue = issues[pr.IssueID]
|
||||
/*
|
||||
Old code:
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
|
||||
It's worth panic because it's almost impossible to happen under normal use.
|
||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||
*/
|
||||
if pr.Issue == nil {
|
||||
return nil, fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||
}
|
||||
}
|
||||
pr.Issue.PullRequest = pr
|
||||
if pr.Issue.Repo == nil {
|
||||
pr.Issue.Repo = pr.BaseRepo
|
||||
}
|
||||
issueList = append(issueList, pr.Issue)
|
||||
}
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// GetIssueIDs returns all issue ids
|
||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||
issueIDs := make([]int64, 0, len(prs))
|
||||
for i := range prs {
|
||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||
}
|
||||
return issueIDs
|
||||
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||
if pr.Issue == nil {
|
||||
return pr.IssueID, pr.IssueID > 0
|
||||
}
|
||||
return 0, false
|
||||
})
|
||||
}
|
||||
|
||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||
|
@ -92,7 +92,7 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
||||
|
||||
// Here to catch weird edge-cases where column constraints above are
|
||||
// not applied by the DB backend
|
||||
_, err := x.Exec("UPDATE repository set object_format_name = 'sha1' WHERE object_format_name = '' or object_format_name IS NULL")
|
||||
_, err := x.Exec("UPDATE `repository` set `object_format_name` = 'sha1' WHERE `object_format_name` = '' or `object_format_name` IS NULL")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,13 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
|
||||
return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
|
||||
}
|
||||
|
||||
// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
// AvatarLink returns the full avatar url with http host or the empty string if the repo doesn't have an avatar.
|
||||
//
|
||||
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
func (repo *Repository) AvatarLink(ctx context.Context) string {
|
||||
return httplib.MakeAbsoluteURL(ctx, repo.relAvatarLink(ctx))
|
||||
relLink := repo.relAvatarLink(ctx)
|
||||
if relLink != "" {
|
||||
return httplib.MakeAbsoluteURL(ctx, relLink)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -856,6 +856,10 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
|
||||
// GetUserByIDs returns the user objects by given IDs if exists.
|
||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users := make([]*User, 0, len(ids))
|
||||
err := db.GetEngine(ctx).In("id", ids).
|
||||
Table("user").
|
||||
|
@ -169,13 +169,18 @@ func TestRender_links(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
// Text that should be turned into URL
|
||||
|
||||
defaultCustom := setting.Markdown.CustomURLSchemes
|
||||
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
||||
markup.ResetDefaultSanitizerForTesting()
|
||||
defer func() {
|
||||
setting.Markdown.CustomURLSchemes = oldCustomURLSchemes
|
||||
markup.ResetDefaultSanitizerForTesting()
|
||||
markup.CustomLinkURLSchemes(oldCustomURLSchemes)
|
||||
}()
|
||||
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
|
||||
// Text that should be turned into URL
|
||||
test(
|
||||
"https://www.example.com",
|
||||
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
||||
@ -259,11 +264,6 @@ func TestRender_links(t *testing.T) {
|
||||
test(
|
||||
"ftps://gitea.com",
|
||||
`<p>ftps://gitea.com</p>`)
|
||||
|
||||
// Restore previous settings
|
||||
setting.Markdown.CustomURLSchemes = defaultCustom
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
||||
func TestRender_email(t *testing.T) {
|
||||
|
@ -47,7 +47,6 @@ func Init(ph *ProcessorHelper) {
|
||||
DefaultProcessorHelper = *ph
|
||||
}
|
||||
|
||||
NewSanitizer()
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
@ -5,13 +5,9 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
@ -21,211 +17,35 @@ type Sanitizer struct {
|
||||
defaultPolicy *bluemonday.Policy
|
||||
descriptionPolicy *bluemonday.Policy
|
||||
rendererPolicies map[string]*bluemonday.Policy
|
||||
init sync.Once
|
||||
allowAllRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
sanitizer = &Sanitizer{}
|
||||
allowAllRegex = regexp.MustCompile(".+")
|
||||
defaultSanitizer *Sanitizer
|
||||
defaultSanitizerOnce sync.Once
|
||||
)
|
||||
|
||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
||||
// entire application lifecycle.
|
||||
func NewSanitizer() {
|
||||
sanitizer.init.Do(func() {
|
||||
InitializeSanitizer()
|
||||
})
|
||||
}
|
||||
|
||||
// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings
|
||||
func InitializeSanitizer() {
|
||||
sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
|
||||
sanitizer.defaultPolicy = createDefaultPolicy()
|
||||
sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
|
||||
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := createDefaultPolicy()
|
||||
addSanitizerRules(policy, sanitizerRules)
|
||||
sanitizer.rendererPolicies[name] = policy
|
||||
func GetDefaultSanitizer() *Sanitizer {
|
||||
defaultSanitizerOnce.Do(func() {
|
||||
defaultSanitizer = &Sanitizer{
|
||||
rendererPolicies: map[string]*bluemonday.Policy{},
|
||||
allowAllRegex: regexp.MustCompile(".+"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
} else {
|
||||
policy.AllowURLSchemesMatching(allowAllRegex)
|
||||
|
||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||
disallowScheme := func(*url.URL) bool {
|
||||
return false
|
||||
}
|
||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||
"charoff", "charset", "checked",
|
||||
"clear", "cols", "colspan", "color",
|
||||
"compact", "coords", "datetime", "dir",
|
||||
"disabled", "enctype", "for", "frame",
|
||||
"headers", "height", "hreflang",
|
||||
"hspace", "ismap", "label", "lang",
|
||||
"maxlength", "media", "method",
|
||||
"multiple", "name", "nohref", "noshade",
|
||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope",
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
|
||||
// Custom keyword markup
|
||||
addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||
// repository descriptions.
|
||||
func createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.NewPolicy()
|
||||
|
||||
// Allow italics and bold.
|
||||
policy.AllowElements("i", "b", "em", "strong")
|
||||
|
||||
// Allow code.
|
||||
policy.AllowElements("code")
|
||||
|
||||
// Allow links
|
||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||
policy.AllowAttrs("aria-label").OnElements("span")
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.AllowDataURIImages {
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
|
||||
defaultSanitizer.rendererPolicies[name] = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
|
||||
})
|
||||
return defaultSanitizer
|
||||
}
|
||||
|
||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||
func SanitizeDescription(s string) string {
|
||||
NewSanitizer()
|
||||
return sanitizer.descriptionPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||
func Sanitize(s string) string {
|
||||
NewSanitizer()
|
||||
return sanitizer.defaultPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// SanitizeReader sanitizes a Reader
|
||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||
NewSanitizer()
|
||||
policy, exist := sanitizer.rendererPolicies[renderer]
|
||||
if !exist {
|
||||
policy = sanitizer.defaultPolicy
|
||||
}
|
||||
return policy.SanitizeReaderToWriter(r, w)
|
||||
func ResetDefaultSanitizerForTesting() {
|
||||
defaultSanitizer = nil
|
||||
defaultSanitizerOnce = sync.Once{}
|
||||
}
|
||||
|
25
modules/markup/sanitizer_custom.go
Normal file
25
modules/markup/sanitizer_custom.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.AllowDataURIImages {
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
modules/markup/sanitizer_default.go
Normal file
146
modules/markup/sanitizer_default.go
Normal file
@ -0,0 +1,146 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
} else {
|
||||
policy.AllowURLSchemesMatching(st.allowAllRegex)
|
||||
|
||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||
disallowScheme := func(*url.URL) bool {
|
||||
return false
|
||||
}
|
||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||
"charoff", "charset", "checked",
|
||||
"clear", "cols", "colspan", "color",
|
||||
"compact", "coords", "datetime", "dir",
|
||||
"disabled", "enctype", "for", "frame",
|
||||
"headers", "height", "hreflang",
|
||||
"hspace", "ismap", "label", "lang",
|
||||
"maxlength", "media", "method",
|
||||
"multiple", "name", "nohref", "noshade",
|
||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope",
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
|
||||
// Custom keyword markup
|
||||
defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||
func Sanitize(s string) string {
|
||||
return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// SanitizeReader sanitizes a Reader
|
||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||
policy, exist := GetDefaultSanitizer().rendererPolicies[renderer]
|
||||
if !exist {
|
||||
policy = GetDefaultSanitizer().defaultPolicy
|
||||
}
|
||||
return policy.SanitizeReaderToWriter(r, w)
|
||||
}
|
@ -5,18 +5,16 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Sanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
func TestSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
// Regular
|
||||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
|
||||
"<scrİpt><script>alert(document.domain)</script></scrİpt>", "<script>alert(document.domain)</script>",
|
||||
|
||||
// Code highlighting class
|
||||
`<code class="random string"></code>`, `<code></code>`,
|
||||
@ -72,34 +70,3 @@ func Test_Sanitizer(t *testing.T) {
|
||||
assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionSanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
|
||||
testCases := []string{
|
||||
`<h1>Title</h1>`, `Title`,
|
||||
`<img src='img.png' alt='image'>`, ``,
|
||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<br>`, ``,
|
||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
|
||||
`<mark>Important!</mark>`, `Important!`,
|
||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||
`<input type="hidden">`, ``,
|
||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeNonEscape(t *testing.T) {
|
||||
descStr := "<scrİpt><script>alert(document.domain)</script></scrİpt>"
|
||||
|
||||
output := template.HTML(Sanitize(descStr))
|
||||
if strings.Contains(string(output), "<script>") {
|
||||
t.Errorf("un-escaped <script> in output: %q", output)
|
||||
}
|
||||
}
|
37
modules/markup/sanitizer_description.go
Normal file
37
modules/markup/sanitizer_description.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||
// repository descriptions.
|
||||
func (st *Sanitizer) createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.NewPolicy()
|
||||
policy.AllowStandardURLs()
|
||||
|
||||
// Allow italics and bold.
|
||||
policy.AllowElements("i", "b", "em", "strong")
|
||||
|
||||
// Allow code.
|
||||
policy.AllowElements("code")
|
||||
|
||||
// Allow links
|
||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||
policy.AllowAttrs("aria-label").OnElements("span")
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||
func SanitizeDescription(s string) string {
|
||||
return GetDefaultSanitizer().descriptionPolicy.Sanitize(s)
|
||||
}
|
31
modules/markup/sanitizer_description_test.go
Normal file
31
modules/markup/sanitizer_description_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDescriptionSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
`<h1>Title</h1>`, `Title`,
|
||||
`<img src='img.png' alt='image'>`, ``,
|
||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<br>`, ``,
|
||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">https://example.com</a>`,
|
||||
`<a href="data:1234">data</a>`, `data`,
|
||||
`<mark>Important!</mark>`, `Important!`,
|
||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||
`<input type="hidden">`, ``,
|
||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||
}
|
||||
}
|
@ -116,23 +116,39 @@ func ListPullRequests(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
apiPrs := make([]*api.PullRequest, len(prs))
|
||||
// NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo
|
||||
if err := prs.LoadRepositories(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
|
||||
return
|
||||
}
|
||||
issueList, err := prs.LoadIssues(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issueList.LoadLabels(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadLabels", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadPosters(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadPoster", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadAttachments(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadMilestones(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadMilestones", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadAssignees(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAssignees", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range prs {
|
||||
if err = prs[i].LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadAttributes(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadBaseRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
||||
return
|
||||
}
|
||||
if err = prs[i].LoadHeadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
|
||||
}
|
||||
|
||||
|
@ -79,9 +79,8 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
|
||||
Page: page,
|
||||
PageSize: 30,
|
||||
},
|
||||
Status: actions_model.StatusUnknown, // Unknown means all
|
||||
IDOrderDesc: true,
|
||||
RunnerID: runner.ID,
|
||||
Status: actions_model.StatusUnknown, // Unknown means all
|
||||
RunnerID: runner.ID,
|
||||
}
|
||||
|
||||
tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)
|
||||
|
@ -31,15 +31,15 @@ func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.
|
||||
}
|
||||
|
||||
func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadAttachments(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
|
||||
apiIssue := &api.Issue{
|
||||
ID: issue.ID,
|
||||
@ -63,6 +63,9 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
|
||||
}
|
||||
apiIssue.URL = issue.APIURL(ctx)
|
||||
apiIssue.HTMLURL = issue.HTMLURL()
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
||||
apiIssue.Repo = &api.RepositoryMeta{
|
||||
ID: issue.Repo.ID,
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -44,7 +45,16 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
return nil
|
||||
}
|
||||
|
||||
p, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
|
||||
var doerID int64
|
||||
if doer != nil {
|
||||
doerID = doer.ID
|
||||
}
|
||||
|
||||
const repoDoerPermCacheKey = "repo_doer_perm_cache"
|
||||
p, err := cache.GetWithContextCache(ctx, repoDoerPermCacheKey, fmt.Sprintf("%d_%d", pr.BaseRepoID, doerID),
|
||||
func() (access_model.Permission, error) {
|
||||
return access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
|
@ -39,7 +39,8 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, issue.Assignees)
|
||||
|
||||
// Check they're gone
|
||||
// Reload to check they're gone
|
||||
issue.ResetAttributesLoaded()
|
||||
assert.NoError(t, issue.LoadAssignees(db.DefaultContext))
|
||||
assert.Empty(t, issue.Assignees)
|
||||
assert.Empty(t, issue.Assignee)
|
||||
|
Loading…
Reference in New Issue
Block a user