mirror of
https://github.com/go-gitea/gitea.git
synced 2024-10-01 03:36:12 -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:
|
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.
|
- `{owner}`: The owner of the repository.
|
||||||
|
@ -54,7 +54,6 @@ type FindTaskOptions struct {
|
|||||||
UpdatedBefore timeutil.TimeStamp
|
UpdatedBefore timeutil.TimeStamp
|
||||||
StartedBefore timeutil.TimeStamp
|
StartedBefore timeutil.TimeStamp
|
||||||
RunnerID int64
|
RunnerID int64
|
||||||
IDOrderDesc bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindTaskOptions) ToConds() builder.Cond {
|
func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||||
@ -84,8 +83,5 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindTaskOptions) ToOrders() string {
|
func (opts FindTaskOptions) ToOrders() string {
|
||||||
if opts.IDOrderDesc {
|
return "`id` DESC"
|
||||||
return "`id` DESC"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
@ -107,17 +107,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
|
|||||||
|
|
||||||
func (opts FindBranchOptions) ToOrders() string {
|
func (opts FindBranchOptions) ToOrders() string {
|
||||||
orderBy := opts.OrderBy
|
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 == "" {
|
if orderBy == "" {
|
||||||
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
// 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
|
return orderBy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,23 +27,27 @@ func init() {
|
|||||||
|
|
||||||
// LoadAssignees load assignees of this issue.
|
// LoadAssignees load assignees of this issue.
|
||||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||||
|
if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset maybe preexisting assignees
|
// Reset maybe preexisting assignees
|
||||||
issue.Assignees = []*user_model.User{}
|
issue.Assignees = []*user_model.User{}
|
||||||
issue.Assignee = nil
|
issue.Assignee = nil
|
||||||
|
|
||||||
err = db.GetEngine(ctx).Table("`user`").
|
if err = db.GetEngine(ctx).Table("`user`").
|
||||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||||
Find(&issue.Assignees)
|
Find(&issue.Assignees); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issue.isAssigneeLoaded = true
|
||||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||||
if len(issue.Assignees) > 0 {
|
if len(issue.Assignees) > 0 {
|
||||||
issue.Assignee = issue.Assignees[0]
|
issue.Assignee = issue.Assignees[0]
|
||||||
}
|
}
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||||
|
@ -16,25 +16,25 @@ import (
|
|||||||
// CommentList defines a list of comments
|
// CommentList defines a list of comments
|
||||||
type CommentList []*Comment
|
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
|
// LoadPosters loads posters
|
||||||
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||||
if len(comments) == 0 {
|
if len(comments) == 0 {
|
||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
if comment.Poster == nil {
|
||||||
|
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -98,32 +98,35 @@ var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already
|
|||||||
|
|
||||||
// Issue represents an issue or pull request of repository.
|
// Issue represents an issue or pull request of repository.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||||
PosterID int64 `xorm:"INDEX"`
|
PosterID int64 `xorm:"INDEX"`
|
||||||
Poster *user_model.User `xorm:"-"`
|
Poster *user_model.User `xorm:"-"`
|
||||||
OriginalAuthor string
|
OriginalAuthor string
|
||||||
OriginalAuthorID int64 `xorm:"index"`
|
OriginalAuthorID int64 `xorm:"index"`
|
||||||
Title string `xorm:"name"`
|
Title string `xorm:"name"`
|
||||||
Content string `xorm:"LONGTEXT"`
|
Content string `xorm:"LONGTEXT"`
|
||||||
RenderedContent template.HTML `xorm:"-"`
|
RenderedContent template.HTML `xorm:"-"`
|
||||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
Labels []*Label `xorm:"-"`
|
Labels []*Label `xorm:"-"`
|
||||||
MilestoneID int64 `xorm:"INDEX"`
|
isLabelsLoaded bool `xorm:"-"`
|
||||||
Milestone *Milestone `xorm:"-"`
|
MilestoneID int64 `xorm:"INDEX"`
|
||||||
Project *project_model.Project `xorm:"-"`
|
Milestone *Milestone `xorm:"-"`
|
||||||
Priority int
|
isMilestoneLoaded bool `xorm:"-"`
|
||||||
AssigneeID int64 `xorm:"-"`
|
Project *project_model.Project `xorm:"-"`
|
||||||
Assignee *user_model.User `xorm:"-"`
|
Priority int
|
||||||
IsClosed bool `xorm:"INDEX"`
|
AssigneeID int64 `xorm:"-"`
|
||||||
IsRead bool `xorm:"-"`
|
Assignee *user_model.User `xorm:"-"`
|
||||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
isAssigneeLoaded bool `xorm:"-"`
|
||||||
PullRequest *PullRequest `xorm:"-"`
|
IsClosed bool `xorm:"INDEX"`
|
||||||
NumComments int
|
IsRead bool `xorm:"-"`
|
||||||
Ref string
|
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||||
PinOrder int `xorm:"DEFAULT 0"`
|
PullRequest *PullRequest `xorm:"-"`
|
||||||
|
NumComments int
|
||||||
|
Ref string
|
||||||
|
PinOrder int `xorm:"DEFAULT 0"`
|
||||||
|
|
||||||
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
|
||||||
@ -131,11 +134,12 @@ type Issue struct {
|
|||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||||
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
|
||||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||||
Comments CommentList `xorm:"-"`
|
isAttachmentsLoaded bool `xorm:"-"`
|
||||||
Reactions ReactionList `xorm:"-"`
|
Comments CommentList `xorm:"-"`
|
||||||
TotalTrackedTime int64 `xorm:"-"`
|
Reactions ReactionList `xorm:"-"`
|
||||||
Assignees []*user_model.User `xorm:"-"`
|
TotalTrackedTime int64 `xorm:"-"`
|
||||||
|
Assignees []*user_model.User `xorm:"-"`
|
||||||
|
|
||||||
// IsLocked limits commenting abilities to users on an issue
|
// IsLocked limits commenting abilities to users on an issue
|
||||||
// with write access
|
// with write access
|
||||||
@ -187,6 +191,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
|||||||
return nil
|
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
|
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
||||||
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
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.
|
// LoadMilestone load milestone of this issue.
|
||||||
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
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)
|
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
||||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
||||||
}
|
}
|
||||||
|
issue.isMilestoneLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -327,11 +345,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.Attachments == nil {
|
if err = issue.LoadAttachments(ctx); err != nil {
|
||||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
return err
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.loadComments(ctx); err != nil {
|
if err = issue.loadComments(ctx); err != nil {
|
||||||
@ -350,6 +365,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||||||
return issue.loadReactions(ctx)
|
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
|
// GetIsRead load the `IsRead` field of the issue
|
||||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issue.isLabelsLoaded = false
|
||||||
issue.Labels = nil
|
issue.Labels = nil
|
||||||
if err = issue.LoadLabels(ctx); err != nil {
|
if err = issue.LoadLabels(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reload all labels
|
||||||
|
issue.isLabelsLoaded = false
|
||||||
issue.Labels = nil
|
issue.Labels = nil
|
||||||
if err = issue.LoadLabels(ctx); err != nil {
|
if err = issue.LoadLabels(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
|||||||
|
|
||||||
// LoadLabels loads labels
|
// LoadLabels loads labels
|
||||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
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)
|
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||||
}
|
}
|
||||||
|
issue.isLabelsLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -72,29 +72,29 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
|
|||||||
return repo_model.ValuesRepository(repoMaps), nil
|
return repo_model.ValuesRepository(repoMaps), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) getPosterIDs() []int64 {
|
func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||||
return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
|
||||||
return issue.PosterID, true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
if issue.Poster == nil {
|
||||||
|
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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))
|
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||||
left := len(posterIDs)
|
left := len(posterIDs)
|
||||||
for left > 0 {
|
for left > 0 {
|
||||||
@ -136,7 +136,7 @@ func (issues IssueList) getIssueIDs() []int64 {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
func (issues IssueList) LoadLabels(ctx context.Context) error {
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -168,7 +168,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||||||
err = rows.Scan(&labelIssue)
|
err = rows.Scan(&labelIssue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err1 := rows.Close(); err1 != 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
|
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.
|
// When there are no rows left and we try to close it.
|
||||||
// Since that is not relevant for us, we can safely ignore it.
|
// Since that is not relevant for us, we can safely ignore it.
|
||||||
if err1 := rows.Close(); err1 != nil {
|
if err1 := rows.Close(); err1 != nil {
|
||||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||||
}
|
}
|
||||||
left -= limit
|
left -= limit
|
||||||
issueIDs = issueIDs[limit:]
|
issueIDs = issueIDs[limit:]
|
||||||
@ -185,6 +185,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Labels = issueLabels[issue.ID]
|
issue.Labels = issueLabels[issue.ID]
|
||||||
|
issue.isLabelsLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
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()
|
milestoneIDs := issues.getMilestoneIDs()
|
||||||
if len(milestoneIDs) == 0 {
|
if len(milestoneIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -220,6 +221,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
|||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||||
|
issue.isMilestoneLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -263,7 +265,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
func (issues IssueList) LoadAssignees(ctx context.Context) error {
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -310,6 +312,10 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
|
|||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Assignees = assignees[issue.ID]
|
issue.Assignees = assignees[issue.ID]
|
||||||
|
if len(issue.Assignees) > 0 {
|
||||||
|
issue.Assignee = issue.Assignees[0]
|
||||||
|
}
|
||||||
|
issue.isAssigneeLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -413,6 +419,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Attachments = attachments[issue.ID]
|
issue.Attachments = attachments[issue.ID]
|
||||||
|
issue.isAttachmentsLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -538,23 +545,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
|
|||||||
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadPosters(ctx); err != nil {
|
if err := issues.LoadPosters(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadLabels(ctx); err != nil {
|
if err := issues.LoadLabels(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadMilestones(ctx); err != nil {
|
if err := issues.LoadMilestones(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.LoadProjects(ctx); err != nil {
|
if err := issues.LoadProjects(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
|
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)
|
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,10 +159,11 @@ type PullRequest struct {
|
|||||||
|
|
||||||
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
||||||
|
|
||||||
IssueID int64 `xorm:"INDEX"`
|
IssueID int64 `xorm:"INDEX"`
|
||||||
Issue *Issue `xorm:"-"`
|
Issue *Issue `xorm:"-"`
|
||||||
Index int64
|
Index int64
|
||||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||||
|
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||||
|
|
||||||
HeadRepoID int64 `xorm:"INDEX"`
|
HeadRepoID int64 `xorm:"INDEX"`
|
||||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||||
@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
// LoadRequestedReviewers loads the requested reviewers.
|
// LoadRequestedReviewers loads the requested reviewers.
|
||||||
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||||
if len(pr.RequestedReviewers) > 0 {
|
if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
pr.isRequestedReviewersLoaded = true
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,10 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"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
|
// 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 {
|
if opts.Page <= 0 {
|
||||||
opts.Page = 1
|
opts.Page = 1
|
||||||
}
|
}
|
||||||
@ -153,50 +155,93 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
|||||||
// PullRequestList defines a list of pull requests
|
// PullRequestList defines a list of pull requests
|
||||||
type PullRequestList []*PullRequest
|
type PullRequestList []*PullRequest
|
||||||
|
|
||||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||||
if len(prs) == 0 {
|
repoIDs := make(container.Set[int64])
|
||||||
return nil
|
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.
|
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||||
issueIDs := prs.GetIssueIDs()
|
repoIDs := prs.getRepositoryIDs()
|
||||||
issues := make([]*Issue, 0, len(issueIDs))
|
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||||
if err := db.GetEngine(ctx).
|
if err := db.GetEngine(ctx).
|
||||||
Where("id > 0").
|
In("id", repoIDs).
|
||||||
In("id", issueIDs).
|
Find(&reposMap); err != nil {
|
||||||
Find(&issues); err != nil {
|
return fmt.Errorf("find repos: %w", err)
|
||||||
return fmt.Errorf("find issues: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
set := make(map[int64]*Issue)
|
|
||||||
for i := range issues {
|
|
||||||
set[issues[i].ID] = issues[i]
|
|
||||||
}
|
}
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
pr.Issue = set[pr.IssueID]
|
if pr.BaseRepo == nil {
|
||||||
/*
|
pr.BaseRepo = reposMap[pr.BaseRepoID]
|
||||||
Old code:
|
}
|
||||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
if pr.HeadRepo == nil {
|
||||||
|
pr.HeadRepo = reposMap[pr.HeadRepoID]
|
||||||
It's worth panic because it's almost impossible to happen under normal use.
|
pr.isHeadRepoLoaded = true
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
pr.Issue.PullRequest = pr
|
|
||||||
}
|
}
|
||||||
return nil
|
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
|
// GetIssueIDs returns all issue ids
|
||||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||||
issueIDs := make([]int64, 0, len(prs))
|
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||||
for i := range prs {
|
if pr.Issue == nil {
|
||||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
return pr.IssueID, pr.IssueID > 0
|
||||||
}
|
}
|
||||||
return issueIDs
|
return 0, false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
// 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
|
// Here to catch weird edge-cases where column constraints above are
|
||||||
// not applied by the DB backend
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,13 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
|
|||||||
return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
|
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 {
|
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.
|
// GetUserByIDs returns the user objects by given IDs if exists.
|
||||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
users := make([]*User, 0, len(ids))
|
users := make([]*User, 0, len(ids))
|
||||||
err := db.GetEngine(ctx).In("id", ids).
|
err := db.GetEngine(ctx).In("id", ids).
|
||||||
Table("user").
|
Table("user").
|
||||||
|
@ -169,13 +169,18 @@ func TestRender_links(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
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"}
|
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
|
||||||
markup.InitializeSanitizer()
|
|
||||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||||
|
|
||||||
|
// Text that should be turned into URL
|
||||||
test(
|
test(
|
||||||
"https://www.example.com",
|
"https://www.example.com",
|
||||||
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
`<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(
|
test(
|
||||||
"ftps://gitea.com",
|
"ftps://gitea.com",
|
||||||
`<p>ftps://gitea.com</p>`)
|
`<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) {
|
func TestRender_email(t *testing.T) {
|
||||||
|
@ -47,7 +47,6 @@ func Init(ph *ProcessorHelper) {
|
|||||||
DefaultProcessorHelper = *ph
|
DefaultProcessorHelper = *ph
|
||||||
}
|
}
|
||||||
|
|
||||||
NewSanitizer()
|
|
||||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,9 @@
|
|||||||
package markup
|
package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,211 +17,35 @@ type Sanitizer struct {
|
|||||||
defaultPolicy *bluemonday.Policy
|
defaultPolicy *bluemonday.Policy
|
||||||
descriptionPolicy *bluemonday.Policy
|
descriptionPolicy *bluemonday.Policy
|
||||||
rendererPolicies map[string]*bluemonday.Policy
|
rendererPolicies map[string]*bluemonday.Policy
|
||||||
init sync.Once
|
allowAllRegex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sanitizer = &Sanitizer{}
|
defaultSanitizer *Sanitizer
|
||||||
allowAllRegex = regexp.MustCompile(".+")
|
defaultSanitizerOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
func GetDefaultSanitizer() *Sanitizer {
|
||||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
defaultSanitizerOnce.Do(func() {
|
||||||
// entire application lifecycle.
|
defaultSanitizer = &Sanitizer{
|
||||||
func NewSanitizer() {
|
rendererPolicies: map[string]*bluemonday.Policy{},
|
||||||
sanitizer.init.Do(func() {
|
allowAllRegex: regexp.MustCompile(".+"),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
for name, renderer := range renderers {
|
||||||
}
|
sanitizerRules := renderer.SanitizerRules()
|
||||||
|
if len(sanitizerRules) > 0 {
|
||||||
func createDefaultPolicy() *bluemonday.Policy {
|
policy := defaultSanitizer.createDefaultPolicy()
|
||||||
policy := bluemonday.UGCPolicy()
|
defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
|
||||||
|
defaultSanitizer.rendererPolicies[name] = policy
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
|
||||||
|
defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
|
||||||
|
})
|
||||||
|
return defaultSanitizer
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
func ResetDefaultSanitizerForTesting() {
|
||||||
func SanitizeDescription(s string) string {
|
defaultSanitizer = nil
|
||||||
NewSanitizer()
|
defaultSanitizerOnce = sync.Once{}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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
|
package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Sanitizer(t *testing.T) {
|
func TestSanitizer(t *testing.T) {
|
||||||
NewSanitizer()
|
|
||||||
testCases := []string{
|
testCases := []string{
|
||||||
// Regular
|
// Regular
|
||||||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
|
`<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 highlighting class
|
||||||
`<code class="random string"></code>`, `<code></code>`,
|
`<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]))
|
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))
|
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 {
|
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)
|
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,
|
Page: page,
|
||||||
PageSize: 30,
|
PageSize: 30,
|
||||||
},
|
},
|
||||||
Status: actions_model.StatusUnknown, // Unknown means all
|
Status: actions_model.StatusUnknown, // Unknown means all
|
||||||
IDOrderDesc: true,
|
RunnerID: runner.ID,
|
||||||
RunnerID: runner.ID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)
|
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 {
|
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 {
|
if err := issue.LoadPoster(ctx); err != nil {
|
||||||
return &api.Issue{}
|
return &api.Issue{}
|
||||||
}
|
}
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
return &api.Issue{}
|
return &api.Issue{}
|
||||||
}
|
}
|
||||||
|
if err := issue.LoadAttachments(ctx); err != nil {
|
||||||
|
return &api.Issue{}
|
||||||
|
}
|
||||||
|
|
||||||
apiIssue := &api.Issue{
|
apiIssue := &api.Issue{
|
||||||
ID: issue.ID,
|
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.URL = issue.APIURL(ctx)
|
||||||
apiIssue.HTMLURL = issue.HTMLURL()
|
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.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
||||||
apiIssue.Repo = &api.RepositoryMeta{
|
apiIssue.Repo = &api.RepositoryMeta{
|
||||||
ID: issue.Repo.ID,
|
ID: issue.Repo.ID,
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@ -44,7 +45,16 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
|||||||
return nil
|
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 {
|
if err != nil {
|
||||||
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
||||||
p.AccessMode = perm.AccessModeNone
|
p.AccessMode = perm.AccessModeNone
|
||||||
|
@ -39,7 +39,8 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, issue.Assignees)
|
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.NoError(t, issue.LoadAssignees(db.DefaultContext))
|
||||||
assert.Empty(t, issue.Assignees)
|
assert.Empty(t, issue.Assignees)
|
||||||
assert.Empty(t, issue.Assignee)
|
assert.Empty(t, issue.Assignee)
|
||||||
|
Loading…
Reference in New Issue
Block a user