backport #28213 This PR will fix some missed checks for private repositories' data on web routes and API routes.
This commit is contained in:
		
							parent
							
								
									7f81110461
								
							
						
					
					
						commit
						bc3d8bff73
					
				|  | @ -92,10 +92,9 @@ func CountUserGPGKeys(ctx context.Context, userID int64) (int64, error) { | |||
| 	return db.GetEngine(ctx).Where("owner_id=? AND primary_key_id=''", userID).Count(&GPGKey{}) | ||||
| } | ||||
| 
 | ||||
| // GetGPGKeyByID returns public key by given ID.
 | ||||
| func GetGPGKeyByID(ctx context.Context, keyID int64) (*GPGKey, error) { | ||||
| func GetGPGKeyForUserByID(ctx context.Context, ownerID, keyID int64) (*GPGKey, error) { | ||||
| 	key := new(GPGKey) | ||||
| 	has, err := db.GetEngine(ctx).ID(keyID).Get(key) | ||||
| 	has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", keyID, ownerID).Get(key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
|  | @ -225,7 +224,7 @@ func deleteGPGKey(ctx context.Context, keyID string) (int64, error) { | |||
| 
 | ||||
| // DeleteGPGKey deletes GPG key information in database.
 | ||||
| func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err error) { | ||||
| 	key, err := GetGPGKeyByID(ctx, id) | ||||
| 	key, err := GetGPGKeyForUserByID(ctx, doer.ID, id) | ||||
| 	if err != nil { | ||||
| 		if IsErrGPGKeyNotExist(err) { | ||||
| 			return nil | ||||
|  | @ -233,11 +232,6 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err | |||
| 		return fmt.Errorf("GetPublicKeyByID: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Check if user has access to delete this key.
 | ||||
| 	if !doer.IsAdmin && doer.ID != key.OwnerID { | ||||
| 		return ErrGPGKeyAccessDenied{doer.ID, key.ID} | ||||
| 	} | ||||
| 
 | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|  |  | |||
|  | @ -66,3 +66,12 @@ | |||
|   tree_path: "README.md" | ||||
|   created_unix: 946684812 | ||||
|   invalidated: true | ||||
| 
 | ||||
| - | ||||
|   id: 8 | ||||
|   type: 0 # comment | ||||
|   poster_id: 2 | ||||
|   issue_id: 4 # in repo_id 2 | ||||
|   content: "comment in private pository" | ||||
|   created_unix: 946684811 | ||||
|   updated_unix: 946684811 | ||||
|  |  | |||
|  | @ -61,7 +61,7 @@ | |||
|   priority: 0 | ||||
|   is_closed: true | ||||
|   is_pull: false | ||||
|   num_comments: 0 | ||||
|   num_comments: 1 | ||||
|   created_unix: 946684830 | ||||
|   updated_unix: 978307200 | ||||
|   is_locked: false | ||||
|  |  | |||
|  | @ -1016,6 +1016,7 @@ type FindCommentsOptions struct { | |||
| 	Type        CommentType | ||||
| 	IssueIDs    []int64 | ||||
| 	Invalidated util.OptionalBool | ||||
| 	IsPull      util.OptionalBool | ||||
| } | ||||
| 
 | ||||
| // ToConds implements FindOptions interface
 | ||||
|  | @ -1050,6 +1051,9 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond { | |||
| 	if !opts.Invalidated.IsNone() { | ||||
| 		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()}) | ||||
| 	} | ||||
| 	if opts.IsPull != util.OptionalBoolNone { | ||||
| 		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) | ||||
| 	} | ||||
| 	return cond | ||||
| } | ||||
| 
 | ||||
|  | @ -1057,7 +1061,7 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond { | |||
| func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) { | ||||
| 	comments := make([]*Comment, 0, 10) | ||||
| 	sess := db.GetEngine(ctx).Where(opts.ToConds()) | ||||
| 	if opts.RepoID > 0 { | ||||
| 	if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone { | ||||
| 		sess.Join("INNER", "issue", "issue.id = comment.issue_id") | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -218,9 +218,9 @@ func GetIssueContentHistoryByID(dbCtx context.Context, id int64) (*ContentHistor | |||
| } | ||||
| 
 | ||||
| // GetIssueContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
 | ||||
| func GetIssueContentHistoryAndPrev(dbCtx context.Context, id int64) (history, prevHistory *ContentHistory, err error) { | ||||
| func GetIssueContentHistoryAndPrev(dbCtx context.Context, issueID, id int64) (history, prevHistory *ContentHistory, err error) { | ||||
| 	history = &ContentHistory{} | ||||
| 	has, err := db.GetEngine(dbCtx).ID(id).Get(history) | ||||
| 	has, err := db.GetEngine(dbCtx).Where("id=? AND issue_id=?", id, issueID).Get(history) | ||||
| 	if err != nil { | ||||
| 		log.Error("failed to get issue content history %v. err=%v", id, err) | ||||
| 		return nil, nil, err | ||||
|  |  | |||
|  | @ -58,13 +58,13 @@ func TestContentHistory(t *testing.T) { | |||
| 	hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1) | ||||
| 	assert.False(t, hasHistory2) | ||||
| 
 | ||||
| 	h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6) | ||||
| 	h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 10, 6) | ||||
| 	assert.EqualValues(t, 6, h6.ID) | ||||
| 	assert.EqualValues(t, 5, h6Prev.ID) | ||||
| 
 | ||||
| 	// soft-delete
 | ||||
| 	_ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5) | ||||
| 	h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6) | ||||
| 	h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 10, 6) | ||||
| 	assert.EqualValues(t, 6, h6.ID) | ||||
| 	assert.EqualValues(t, 4, h6Prev.ID) | ||||
| 
 | ||||
|  |  | |||
|  | @ -311,6 +311,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) { | |||
| 	return p, nil | ||||
| } | ||||
| 
 | ||||
| // GetProjectForRepoByID returns the projects in a repository
 | ||||
| func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) { | ||||
| 	p := new(Project) | ||||
| 	has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(p) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrProjectNotExist{ID: id} | ||||
| 	} | ||||
| 	return p, nil | ||||
| } | ||||
| 
 | ||||
| // UpdateProject updates project properties
 | ||||
| func UpdateProject(ctx context.Context, p *Project) error { | ||||
| 	if !IsCardTypeValid(p.CardType) { | ||||
|  |  | |||
|  | @ -207,6 +207,21 @@ func GetReleaseByID(ctx context.Context, id int64) (*Release, error) { | |||
| 	return rel, nil | ||||
| } | ||||
| 
 | ||||
| // GetReleaseForRepoByID returns release with given ID.
 | ||||
| func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, error) { | ||||
| 	rel := new(Release) | ||||
| 	has, err := db.GetEngine(ctx). | ||||
| 		Where("id=? AND repo_id=?", id, repoID). | ||||
| 		Get(rel) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrReleaseNotExist{id, ""} | ||||
| 	} | ||||
| 
 | ||||
| 	return rel, nil | ||||
| } | ||||
| 
 | ||||
| // FindReleasesOptions describes the conditions to Find releases
 | ||||
| type FindReleasesOptions struct { | ||||
| 	db.ListOptions | ||||
|  |  | |||
|  | @ -392,39 +392,40 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error { | |||
| 	return db.Insert(ctx, ws) | ||||
| } | ||||
| 
 | ||||
| // getWebhook uses argument bean as query condition,
 | ||||
| // ID must be specified and do not assign unnecessary fields.
 | ||||
| func getWebhook(bean *Webhook) (*Webhook, error) { | ||||
| 	has, err := db.GetEngine(db.DefaultContext).Get(bean) | ||||
| // GetWebhookByID returns webhook of repository by given ID.
 | ||||
| func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) { | ||||
| 	bean := new(Webhook) | ||||
| 	has, err := db.GetEngine(ctx).ID(id).Get(bean) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrWebhookNotExist{ID: bean.ID} | ||||
| 		return nil, ErrWebhookNotExist{ID: id} | ||||
| 	} | ||||
| 	return bean, nil | ||||
| } | ||||
| 
 | ||||
| // GetWebhookByID returns webhook of repository by given ID.
 | ||||
| func GetWebhookByID(id int64) (*Webhook, error) { | ||||
| 	return getWebhook(&Webhook{ | ||||
| 		ID: id, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // GetWebhookByRepoID returns webhook of repository by given ID.
 | ||||
| func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) { | ||||
| 	return getWebhook(&Webhook{ | ||||
| 		ID:     id, | ||||
| 		RepoID: repoID, | ||||
| 	}) | ||||
| func GetWebhookByRepoID(ctx context.Context, repoID, id int64) (*Webhook, error) { | ||||
| 	webhook := new(Webhook) | ||||
| 	has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(webhook) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrWebhookNotExist{ID: id} | ||||
| 	} | ||||
| 	return webhook, nil | ||||
| } | ||||
| 
 | ||||
| // GetWebhookByOwnerID returns webhook of a user or organization by given ID.
 | ||||
| func GetWebhookByOwnerID(ownerID, id int64) (*Webhook, error) { | ||||
| 	return getWebhook(&Webhook{ | ||||
| 		ID:      id, | ||||
| 		OwnerID: ownerID, | ||||
| 	}) | ||||
| func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, error) { | ||||
| 	webhook := new(Webhook) | ||||
| 	has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", id, ownerID).Get(webhook) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrWebhookNotExist{ID: id} | ||||
| 	} | ||||
| 	return webhook, nil | ||||
| } | ||||
| 
 | ||||
| // ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
 | ||||
|  | @ -482,20 +483,20 @@ func UpdateWebhookLastStatus(w *Webhook) error { | |||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // deleteWebhook uses argument bean as query condition,
 | ||||
| // DeleteWebhookByID uses argument bean as query condition,
 | ||||
| // ID must be specified and do not assign unnecessary fields.
 | ||||
| func deleteWebhook(bean *Webhook) (err error) { | ||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||
| func DeleteWebhookByID(ctx context.Context, id int64) (err error) { | ||||
| 	ctx, committer, err := db.TxContext(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer committer.Close() | ||||
| 
 | ||||
| 	if count, err := db.DeleteByBean(ctx, bean); err != nil { | ||||
| 	if count, err := db.DeleteByID(ctx, id, new(Webhook)); err != nil { | ||||
| 		return err | ||||
| 	} else if count == 0 { | ||||
| 		return ErrWebhookNotExist{ID: bean.ID} | ||||
| 	} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: bean.ID}); err != nil { | ||||
| 		return ErrWebhookNotExist{ID: id} | ||||
| 	} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
|  | @ -503,17 +504,17 @@ func deleteWebhook(bean *Webhook) (err error) { | |||
| } | ||||
| 
 | ||||
| // DeleteWebhookByRepoID deletes webhook of repository by given ID.
 | ||||
| func DeleteWebhookByRepoID(repoID, id int64) error { | ||||
| 	return deleteWebhook(&Webhook{ | ||||
| 		ID:     id, | ||||
| 		RepoID: repoID, | ||||
| 	}) | ||||
| func DeleteWebhookByRepoID(ctx context.Context, repoID, id int64) error { | ||||
| 	if _, err := GetWebhookByRepoID(ctx, repoID, id); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return DeleteWebhookByID(ctx, id) | ||||
| } | ||||
| 
 | ||||
| // DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
 | ||||
| func DeleteWebhookByOwnerID(ownerID, id int64) error { | ||||
| 	return deleteWebhook(&Webhook{ | ||||
| 		ID:      id, | ||||
| 		OwnerID: ownerID, | ||||
| 	}) | ||||
| func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error { | ||||
| 	if _, err := GetWebhookByOwnerID(ctx, ownerID, id); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return DeleteWebhookByID(ctx, id) | ||||
| } | ||||
|  |  | |||
|  | @ -101,22 +101,22 @@ func TestCreateWebhook(t *testing.T) { | |||
| 
 | ||||
| func TestGetWebhookByRepoID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	hook, err := GetWebhookByRepoID(1, 1) | ||||
| 	hook, err := GetWebhookByRepoID(db.DefaultContext, 1, 1) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, int64(1), hook.ID) | ||||
| 
 | ||||
| 	_, err = GetWebhookByRepoID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	_, err = GetWebhookByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
| 
 | ||||
| func TestGetWebhookByOwnerID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	hook, err := GetWebhookByOwnerID(3, 3) | ||||
| 	hook, err := GetWebhookByOwnerID(db.DefaultContext, 3, 3) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, int64(3), hook.ID) | ||||
| 
 | ||||
| 	_, err = GetWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	_, err = GetWebhookByOwnerID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
|  | @ -174,10 +174,10 @@ func TestUpdateWebhook(t *testing.T) { | |||
| func TestDeleteWebhookByRepoID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1}) | ||||
| 	assert.NoError(t, DeleteWebhookByRepoID(1, 2)) | ||||
| 	assert.NoError(t, DeleteWebhookByRepoID(db.DefaultContext, 1, 2)) | ||||
| 	unittest.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1}) | ||||
| 
 | ||||
| 	err := DeleteWebhookByRepoID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	err := DeleteWebhookByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
|  | @ -185,10 +185,10 @@ func TestDeleteWebhookByRepoID(t *testing.T) { | |||
| func TestDeleteWebhookByOwnerID(t *testing.T) { | ||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
| 	unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) | ||||
| 	assert.NoError(t, DeleteWebhookByOwnerID(3, 3)) | ||||
| 	assert.NoError(t, DeleteWebhookByOwnerID(db.DefaultContext, 3, 3)) | ||||
| 	unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3}) | ||||
| 
 | ||||
| 	err := DeleteWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	err := DeleteWebhookByOwnerID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrWebhookNotExist(err)) | ||||
| } | ||||
|  |  | |||
|  | @ -1258,8 +1258,8 @@ func Routes() *web.Route { | |||
| 			m.Group("/{username}/{reponame}", func() { | ||||
| 				m.Group("/issues", func() { | ||||
| 					m.Combo("").Get(repo.ListIssues). | ||||
| 						Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) | ||||
| 					m.Get("/pinned", repo.ListPinnedIssues) | ||||
| 						Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue) | ||||
| 					m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues) | ||||
| 					m.Group("/comments", func() { | ||||
| 						m.Get("", repo.ListRepoIssueComments) | ||||
| 						m.Group("/{id}", func() { | ||||
|  |  | |||
|  | @ -301,7 +301,7 @@ func DeleteHook(ctx *context.APIContext) { | |||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 	//   "404":
 | ||||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 	if err := webhook.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil { | ||||
| 	if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil { | ||||
| 		if webhook.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 		} else { | ||||
|  |  | |||
|  | @ -462,6 +462,24 @@ func ListIssues(ctx *context.APIContext) { | |||
| 		isPull = util.OptionalBoolNone | ||||
| 	} | ||||
| 
 | ||||
| 	if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if isPull == util.OptionalBoolNone { | ||||
| 		canReadIssues := ctx.Repo.CanRead(unit.TypeIssues) | ||||
| 		canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests) | ||||
| 		if !canReadIssues && !canReadPulls { | ||||
| 			ctx.NotFound() | ||||
| 			return | ||||
| 		} else if !canReadIssues { | ||||
| 			isPull = util.OptionalBoolTrue | ||||
| 		} else if !canReadPulls { | ||||
| 			isPull = util.OptionalBoolFalse | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// FIXME: we should be more efficient here
 | ||||
| 	createdByID := getUserIDForFilter(ctx, "created_by") | ||||
| 	if ctx.Written() { | ||||
|  | @ -593,6 +611,10 @@ func GetIssue(ctx *context.APIContext) { | |||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue)) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,9 +12,11 @@ import ( | |||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	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/context" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| 	"code.gitea.io/gitea/services/convert" | ||||
|  | @ -71,6 +73,11 @@ func ListIssueComments(ctx *context.APIContext) { | |||
| 		ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	issue.Repo = ctx.Repo.Repository | ||||
| 
 | ||||
| 	opts := &issues_model.FindCommentsOptions{ | ||||
|  | @ -271,12 +278,27 @@ func ListRepoIssueComments(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var isPull util.OptionalBool | ||||
| 	canReadIssue := ctx.Repo.CanRead(unit.TypeIssues) | ||||
| 	canReadPull := ctx.Repo.CanRead(unit.TypePullRequests) | ||||
| 	if canReadIssue && canReadPull { | ||||
| 		isPull = util.OptionalBoolNone | ||||
| 	} else if canReadIssue { | ||||
| 		isPull = util.OptionalBoolFalse | ||||
| 	} else if canReadPull { | ||||
| 		isPull = util.OptionalBoolTrue | ||||
| 	} else { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	opts := &issues_model.FindCommentsOptions{ | ||||
| 		ListOptions: utils.GetListOptions(ctx), | ||||
| 		RepoID:      ctx.Repo.Repository.ID, | ||||
| 		Type:        issues_model.CommentTypeComment, | ||||
| 		Since:       since, | ||||
| 		Before:      before, | ||||
| 		IsPull:      isPull, | ||||
| 	} | ||||
| 
 | ||||
| 	comments, err := issues_model.FindComments(ctx, opts) | ||||
|  | @ -365,6 +387,11 @@ func CreateIssueComment(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { | ||||
| 		ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked"))) | ||||
| 		return | ||||
|  | @ -434,6 +461,11 @@ func GetIssueComment(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Type != issues_model.CommentTypeComment { | ||||
| 		ctx.Status(http.StatusNoContent) | ||||
| 		return | ||||
|  | @ -552,7 +584,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) { | ||||
| 	if err := comment.LoadIssue(ctx); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "LoadIssue", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.Status(http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { | ||||
| 		ctx.Status(http.StatusForbidden) | ||||
| 		return | ||||
| 	} | ||||
|  | @ -655,7 +697,17 @@ func deleteIssueComment(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) { | ||||
| 	if err := comment.LoadIssue(ctx); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "LoadIssue", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.Status(http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { | ||||
| 		ctx.Status(http.StatusForbidden) | ||||
| 		return | ||||
| 	} else if comment.Type != issues_model.CommentTypeComment { | ||||
|  |  | |||
|  | @ -325,6 +325,10 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { | |||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	comment.Issue.Repo = ctx.Repo.Repository | ||||
| 
 | ||||
| 	return comment | ||||
|  |  | |||
|  | @ -61,6 +61,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) { | |||
| 
 | ||||
| 	if err := comment.LoadIssue(ctx); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { | ||||
|  | @ -190,9 +196,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	err = comment.LoadIssue(ctx) | ||||
| 	if err != nil { | ||||
| 	if err = comment.LoadIssue(ctx); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { | ||||
|  |  | |||
|  | @ -159,6 +159,12 @@ func GetDeployKey(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// this check make it more consistent
 | ||||
| 	if key.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err = key.GetContent(); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetContent", err) | ||||
| 		return | ||||
|  |  | |||
|  | @ -49,13 +49,12 @@ func GetRelease(ctx *context.APIContext) { | |||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 
 | ||||
| 	id := ctx.ParamsInt64(":id") | ||||
| 	release, err := repo_model.GetReleaseByID(ctx, id) | ||||
| 	release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) | ||||
| 	if err != nil && !repo_model.IsErrReleaseNotExist(err) { | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil && repo_model.IsErrReleaseNotExist(err) || | ||||
| 		release.IsTag || release.RepoID != ctx.Repo.Repository.ID { | ||||
| 	if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
|  | @ -315,13 +314,12 @@ func EditRelease(ctx *context.APIContext) { | |||
| 
 | ||||
| 	form := web.GetForm(ctx).(*api.EditReleaseOption) | ||||
| 	id := ctx.ParamsInt64(":id") | ||||
| 	rel, err := repo_model.GetReleaseByID(ctx, id) | ||||
| 	rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) | ||||
| 	if err != nil && !repo_model.IsErrReleaseNotExist(err) { | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil && repo_model.IsErrReleaseNotExist(err) || | ||||
| 		rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID { | ||||
| 	if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
|  | @ -393,17 +391,16 @@ func DeleteRelease(ctx *context.APIContext) { | |||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 
 | ||||
| 	id := ctx.ParamsInt64(":id") | ||||
| 	rel, err := repo_model.GetReleaseByID(ctx, id) | ||||
| 	rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) | ||||
| 	if err != nil && !repo_model.IsErrReleaseNotExist(err) { | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil && repo_model.IsErrReleaseNotExist(err) || | ||||
| 		rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID { | ||||
| 	if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 	if err := release_service.DeleteReleaseByID(ctx, id, ctx.Doer, false); err != nil { | ||||
| 	if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, false); err != nil { | ||||
| 		if models.IsErrProtectedTagName(err) { | ||||
| 			ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag") | ||||
| 			return | ||||
|  |  | |||
|  | @ -17,6 +17,23 @@ import ( | |||
| 	"code.gitea.io/gitea/services/convert" | ||||
| ) | ||||
| 
 | ||||
| func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool { | ||||
| 	release, err := repo_model.GetReleaseByID(ctx, releaseID) | ||||
| 	if err != nil { | ||||
| 		if repo_model.IsErrReleaseNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 			return false | ||||
| 		} | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) | ||||
| 		return false | ||||
| 	} | ||||
| 	if release.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound() | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| // GetReleaseAttachment gets a single attachment of the release
 | ||||
| func GetReleaseAttachment(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoGetReleaseAttachment
 | ||||
|  | @ -54,6 +71,10 @@ func GetReleaseAttachment(ctx *context.APIContext) { | |||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 
 | ||||
| 	releaseID := ctx.ParamsInt64(":id") | ||||
| 	if !checkReleaseMatchRepo(ctx, releaseID) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	attachID := ctx.ParamsInt64(":attachment_id") | ||||
| 	attach, err := repo_model.GetAttachmentByID(ctx, attachID) | ||||
| 	if err != nil { | ||||
|  | @ -176,13 +197,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { | |||
| 
 | ||||
| 	// Check if release exists an load release
 | ||||
| 	releaseID := ctx.ParamsInt64(":id") | ||||
| 	release, err := repo_model.GetReleaseByID(ctx, releaseID) | ||||
| 	if err != nil { | ||||
| 		if repo_model.IsErrReleaseNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) | ||||
| 	if !checkReleaseMatchRepo(ctx, releaseID) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
|  | @ -203,7 +218,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { | |||
| 	attach, err := attachment.UploadAttachment(file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{ | ||||
| 		Name:       filename, | ||||
| 		UploaderID: ctx.Doer.ID, | ||||
| 		RepoID:     release.RepoID, | ||||
| 		RepoID:     ctx.Repo.Repository.ID, | ||||
| 		ReleaseID:  releaseID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
|  | @ -264,6 +279,10 @@ func EditReleaseAttachment(ctx *context.APIContext) { | |||
| 
 | ||||
| 	// Check if release exists an load release
 | ||||
| 	releaseID := ctx.ParamsInt64(":id") | ||||
| 	if !checkReleaseMatchRepo(ctx, releaseID) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	attachID := ctx.ParamsInt64(":attachment_id") | ||||
| 	attach, err := repo_model.GetAttachmentByID(ctx, attachID) | ||||
| 	if err != nil { | ||||
|  | @ -328,6 +347,10 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { | |||
| 
 | ||||
| 	// Check if release exists an load release
 | ||||
| 	releaseID := ctx.ParamsInt64(":id") | ||||
| 	if !checkReleaseMatchRepo(ctx, releaseID) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	attachID := ctx.ParamsInt64(":attachment_id") | ||||
| 	attach, err := repo_model.GetAttachmentByID(ctx, attachID) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ func DeleteReleaseByTag(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err = releaseservice.DeleteReleaseByID(ctx, release.ID, ctx.Doer, false); err != nil { | ||||
| 	if err = releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil { | ||||
| 		if models.IsErrProtectedTagName(err) { | ||||
| 			ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag") | ||||
| 			return | ||||
|  |  | |||
|  | @ -268,7 +268,7 @@ func DeleteTag(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err = releaseservice.DeleteReleaseByID(ctx, tag.ID, ctx.Doer, true); err != nil { | ||||
| 	if err = releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil { | ||||
| 		if models.IsErrProtectedTagName(err) { | ||||
| 			ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag") | ||||
| 			return | ||||
|  |  | |||
|  | @ -343,6 +343,10 @@ func GetOauth2Application(ctx *context.APIContext) { | |||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	if app.UID != ctx.Doer.ID { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	app.ClientSecret = "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ func GetGPGKey(ctx *context.APIContext) { | |||
| 	//   "404":
 | ||||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 
 | ||||
| 	key, err := asymkey_model.GetGPGKeyByID(ctx, ctx.ParamsInt64(":id")) | ||||
| 	key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.ParamsInt64(":id")) | ||||
| 	if err != nil { | ||||
| 		if asymkey_model.IsErrGPGKeyNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
|  |  | |||
|  | @ -62,6 +62,11 @@ func GetHook(ctx *context.APIContext) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Doer.IsAdmin && hook.OwnerID != ctx.Doer.ID { | ||||
| 		ctx.NotFound() | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { | |||
| 
 | ||||
| // GetOwnerHook gets an user or organization webhook. Errors are written to ctx.
 | ||||
| func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) { | ||||
| 	w, err := webhook.GetWebhookByOwnerID(ownerID, hookID) | ||||
| 	w, err := webhook.GetWebhookByOwnerID(ctx, ownerID, hookID) | ||||
| 	if err != nil { | ||||
| 		if webhook.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
|  | @ -68,7 +68,7 @@ func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webh | |||
| // GetRepoHook get a repo's webhook. If there is an error, write to `ctx`
 | ||||
| // accordingly and return the error
 | ||||
| func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhook, error) { | ||||
| 	w, err := webhook.GetWebhookByRepoID(repoID, hookID) | ||||
| 	w, err := webhook.GetWebhookByRepoID(ctx, repoID, hookID) | ||||
| 	if err != nil { | ||||
| 		if webhook.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
|  | @ -401,7 +401,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh | |||
| 
 | ||||
| // DeleteOwnerHook deletes the hook owned by the owner.
 | ||||
| func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(owner.ID, hookID); err != nil { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx, owner.ID, hookID); err != nil { | ||||
| 		if webhook.IsErrWebhookNotExist(err) { | ||||
| 			ctx.NotFound() | ||||
| 		} else { | ||||
|  |  | |||
|  | @ -233,7 +233,7 @@ func Webhooks(ctx *context.Context) { | |||
| 
 | ||||
| // DeleteWebhook response for delete webhook
 | ||||
| func DeleteWebhook(ctx *context.Context) { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) | ||||
|  |  | |||
|  | @ -3091,6 +3091,11 @@ func UpdateCommentContent(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { | ||||
| 		ctx.Error(http.StatusForbidden) | ||||
| 		return | ||||
|  | @ -3157,6 +3162,11 @@ func DeleteComment(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { | ||||
| 		ctx.Error(http.StatusForbidden) | ||||
| 		return | ||||
|  | @ -3283,6 +3293,11 @@ func ChangeCommentReaction(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) { | ||||
| 		if log.IsTrace() { | ||||
| 			if ctx.IsSigned { | ||||
|  | @ -3426,6 +3441,21 @@ func GetCommentAttachments(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := comment.LoadIssue(ctx); err != nil { | ||||
| 		ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if comment.Issue.RepoID != ctx.Repo.Repository.ID { | ||||
| 		ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { | ||||
| 		ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if !comment.Type.HasAttachmentSupport() { | ||||
| 		ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type)) | ||||
| 		return | ||||
|  |  | |||
|  | @ -122,7 +122,7 @@ func GetContentHistoryDetail(ctx *context.Context) { | |||
| 	} | ||||
| 
 | ||||
| 	historyID := ctx.FormInt64("history_id") | ||||
| 	history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID) | ||||
| 	history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, issue.ID, historyID) | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(http.StatusNotFound, map[string]any{ | ||||
| 			"message": "Can not find the content history", | ||||
|  |  | |||
|  | @ -464,7 +464,7 @@ func AddBoardToProjectPost(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) | ||||
| 	project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) | ||||
| 	if err != nil { | ||||
| 		if project_model.IsErrProjectNotExist(err) { | ||||
| 			ctx.NotFound("", nil) | ||||
|  |  | |||
|  | @ -613,7 +613,27 @@ func DeleteTag(ctx *context.Context) { | |||
| } | ||||
| 
 | ||||
| func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) { | ||||
| 	if err := releaseservice.DeleteReleaseByID(ctx, ctx.FormInt64("id"), ctx.Doer, isDelTag); err != nil { | ||||
| 	redirect := func() { | ||||
| 		if isDelTag { | ||||
| 			ctx.JSONRedirect(ctx.Repo.RepoLink + "/tags") | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.JSONRedirect(ctx.Repo.RepoLink + "/releases") | ||||
| 	} | ||||
| 
 | ||||
| 	rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")) | ||||
| 	if err != nil { | ||||
| 		if repo_model.IsErrReleaseNotExist(err) { | ||||
| 			ctx.NotFound("GetReleaseForRepoByID", err) | ||||
| 		} else { | ||||
| 			ctx.Flash.Error("DeleteReleaseByID: " + err.Error()) | ||||
| 			redirect() | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil { | ||||
| 		if models.IsErrProtectedTagName(err) { | ||||
| 			ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) | ||||
| 		} else { | ||||
|  | @ -627,10 +647,5 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if isDelTag { | ||||
| 		ctx.JSONRedirect(ctx.Repo.RepoLink + "/tags") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSONRedirect(ctx.Repo.RepoLink + "/releases") | ||||
| 	redirect() | ||||
| } | ||||
|  |  | |||
|  | @ -589,9 +589,9 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { | |||
| 
 | ||||
| 	var w *webhook.Webhook | ||||
| 	if orCtx.RepoID > 0 { | ||||
| 		w, err = webhook.GetWebhookByRepoID(orCtx.RepoID, ctx.ParamsInt64(":id")) | ||||
| 		w, err = webhook.GetWebhookByRepoID(ctx, orCtx.RepoID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.OwnerID > 0 { | ||||
| 		w, err = webhook.GetWebhookByOwnerID(orCtx.OwnerID, ctx.ParamsInt64(":id")) | ||||
| 		w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.OwnerID, ctx.ParamsInt64(":id")) | ||||
| 	} else if orCtx.IsAdmin { | ||||
| 		w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.ParamsInt64(":id")) | ||||
| 	} | ||||
|  | @ -643,7 +643,7 @@ func WebHooksEdit(ctx *context.Context) { | |||
| // TestWebhook test if web hook is work fine
 | ||||
| func TestWebhook(ctx *context.Context) { | ||||
| 	hookID := ctx.ParamsInt64(":id") | ||||
| 	w, err := webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, hookID) | ||||
| 	w, err := webhook.GetWebhookByRepoID(ctx, ctx.Repo.Repository.ID, hookID) | ||||
| 	if err != nil { | ||||
| 		ctx.Flash.Error("GetWebhookByRepoID: " + err.Error()) | ||||
| 		ctx.Status(http.StatusInternalServerError) | ||||
|  | @ -724,7 +724,7 @@ func ReplayWebhook(ctx *context.Context) { | |||
| 
 | ||||
| // DeleteWebhook delete a webhook
 | ||||
| func DeleteWebhook(ctx *context.Context) { | ||||
| 	if err := webhook.DeleteWebhookByRepoID(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { | ||||
| 	if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteWebhookByRepoID: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ func Webhooks(ctx *context.Context) { | |||
| 
 | ||||
| // DeleteWebhook response for delete webhook
 | ||||
| func DeleteWebhook(ctx *context.Context) { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx.Doer.ID, ctx.FormInt64("id")); err != nil { | ||||
| 	if err := webhook.DeleteWebhookByOwnerID(ctx, ctx.Doer.ID, ctx.FormInt64("id")); err != nil { | ||||
| 		ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) | ||||
| 	} else { | ||||
| 		ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) | ||||
|  |  | |||
|  | @ -291,17 +291,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo | |||
| } | ||||
| 
 | ||||
| // DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
 | ||||
| func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, delTag bool) error { | ||||
| 	rel, err := repo_model.GetReleaseByID(ctx, id) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("GetReleaseByID: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("GetRepositoryByID: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *repo_model.Release, doer *user_model.User, delTag bool) error { | ||||
| 	if delTag { | ||||
| 		protectedTags, err := git_model.GetProtectedTags(ctx, rel.RepoID) | ||||
| 		if err != nil { | ||||
|  | @ -334,19 +324,19 @@ func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, del | |||
| 			}, repository.NewPushCommits()) | ||||
| 		notify_service.DeleteRef(ctx, doer, repo, refName) | ||||
| 
 | ||||
| 		if err := repo_model.DeleteReleaseByID(ctx, id); err != nil { | ||||
| 		if err := repo_model.DeleteReleaseByID(ctx, rel.ID); err != nil { | ||||
| 			return fmt.Errorf("DeleteReleaseByID: %w", err) | ||||
| 		} | ||||
| 	} else { | ||||
| 		rel.IsTag = true | ||||
| 
 | ||||
| 		if err = repo_model.UpdateRelease(ctx, rel); err != nil { | ||||
| 		if err := repo_model.UpdateRelease(ctx, rel); err != nil { | ||||
| 			return fmt.Errorf("Update: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	rel.Repo = repo | ||||
| 	if err = rel.LoadAttributes(ctx); err != nil { | ||||
| 	if err := rel.LoadAttributes(ctx); err != nil { | ||||
| 		return fmt.Errorf("LoadAttributes: %w", err) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ import ( | |||
| 
 | ||||
| // Deliver deliver hook task
 | ||||
| func Deliver(ctx context.Context, t *webhook_model.HookTask) error { | ||||
| 	w, err := webhook_model.GetWebhookByID(t.HookID) | ||||
| 	w, err := webhook_model.GetWebhookByID(ctx, t.HookID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -35,6 +35,14 @@ func TestAPIGetCommentAttachment(t *testing.T) { | |||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID}) | ||||
| 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 
 | ||||
| 	t.Run("UnrelatedCommentID", func(t *testing.T) { | ||||
| 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) | ||||
| 		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 		token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) | ||||
| 		req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	session := loginUser(t, repoOwner.Name) | ||||
| 	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) | ||||
| 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token) | ||||
|  |  | |||
|  | @ -177,12 +177,25 @@ func TestAPIEditComment(t *testing.T) { | |||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 	const newCommentBody = "This is the new comment body" | ||||
| 
 | ||||
| 	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, | ||||
| 	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 8}, | ||||
| 		unittest.Cond("type = ?", issues_model.CommentTypeComment)) | ||||
| 	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) | ||||
| 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 
 | ||||
| 	t.Run("UnrelatedCommentID", func(t *testing.T) { | ||||
| 		// Using the ID of a comment that does not belong to the repository must fail
 | ||||
| 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) | ||||
| 		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 		token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) | ||||
| 		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s", | ||||
| 			repoOwner.Name, repo.Name, comment.ID, token) | ||||
| 		req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ | ||||
| 			"body": newCommentBody, | ||||
| 		}) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) | ||||
| 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s", | ||||
| 		repoOwner.Name, repo.Name, comment.ID, token) | ||||
|  | @ -201,12 +214,22 @@ func TestAPIEditComment(t *testing.T) { | |||
| func TestAPIDeleteComment(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 
 | ||||
| 	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{}, | ||||
| 	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 8}, | ||||
| 		unittest.Cond("type = ?", issues_model.CommentTypeComment)) | ||||
| 	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) | ||||
| 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 
 | ||||
| 	t.Run("UnrelatedCommentID", func(t *testing.T) { | ||||
| 		// Using the ID of a comment that does not belong to the repository must fail
 | ||||
| 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) | ||||
| 		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 		token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) | ||||
| 		req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s", | ||||
| 			repoOwner.Name, repo.Name, comment.ID, token) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) | ||||
| 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s", | ||||
| 		repoOwner.Name, repo.Name, comment.ID, token) | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import ( | |||
| 	auth_model "code.gitea.io/gitea/models/auth" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
|  | @ -107,6 +108,26 @@ func TestAPICommentReactions(t *testing.T) { | |||
| 	}) | ||||
| 	MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 	t.Run("UnrelatedCommentID", func(t *testing.T) { | ||||
| 		// Using the ID of a comment that does not belong to the repository must fail
 | ||||
| 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) | ||||
| 		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | ||||
| 		token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue) | ||||
| 		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s", | ||||
| 			repoOwner.Name, repo.Name, comment.ID, token) | ||||
| 		req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ | ||||
| 			Reaction: "+1", | ||||
| 		}) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 		req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{ | ||||
| 			Reaction: "+1", | ||||
| 		}) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 
 | ||||
| 		req = NewRequestf(t, "GET", urlStr) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	}) | ||||
| 
 | ||||
| 	// Add allowed reaction
 | ||||
| 	req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ | ||||
| 		Reaction: "+1", | ||||
|  |  | |||
|  | @ -72,6 +72,17 @@ func TestCreateReadOnlyDeployKey(t *testing.T) { | |||
| 		Content: rawKeyBody.Key, | ||||
| 		Mode:    perm.AccessModeRead, | ||||
| 	}) | ||||
| 
 | ||||
| 	// Using the ID of a key that does not belong to the repository must fail
 | ||||
| 	{ | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/keys/%d?token=%s", repoOwner.Name, repo.Name, newDeployKey.ID, token)) | ||||
| 		MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		session5 := loginUser(t, "user5") | ||||
| 		token5 := getTokenForLoggedInUser(t, session5, auth_model.AccessTokenScopeWriteRepository) | ||||
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user5/repo4/keys/%d?token=%s", newDeployKey.ID, token5)) | ||||
| 		MakeRequest(t, req, http.StatusNotFound) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCreateReadWriteDeployKey(t *testing.T) { | ||||
|  |  | |||
|  | @ -34,6 +34,6 @@ func TestNodeinfo(t *testing.T) { | |||
| 		assert.Equal(t, "gitea", nodeinfo.Software.Name) | ||||
| 		assert.Equal(t, 25, nodeinfo.Usage.Users.Total) | ||||
| 		assert.Equal(t, 20, nodeinfo.Usage.LocalPosts) | ||||
| 		assert.Equal(t, 2, nodeinfo.Usage.LocalComments) | ||||
| 		assert.Equal(t, 3, nodeinfo.Usage.LocalComments) | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -206,6 +206,56 @@ func TestIssueCommentClose(t *testing.T) { | |||
| 	assert.Equal(t, "Description", val) | ||||
| } | ||||
| 
 | ||||
| func TestIssueCommentDelete(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 	session := loginUser(t, "user2") | ||||
| 	issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description") | ||||
| 	comment1 := "Test comment 1" | ||||
| 	commentID := testIssueAddComment(t, session, issueURL, comment1, "") | ||||
| 	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID}) | ||||
| 	assert.Equal(t, comment1, comment.Content) | ||||
| 
 | ||||
| 	// Using the ID of a comment that does not belong to the repository must fail
 | ||||
| 	req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{ | ||||
| 		"_csrf": GetCSRF(t, session, issueURL), | ||||
| 	}) | ||||
| 	session.MakeRequest(t, req, http.StatusNotFound) | ||||
| 	req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{ | ||||
| 		"_csrf": GetCSRF(t, session, issueURL), | ||||
| 	}) | ||||
| 	session.MakeRequest(t, req, http.StatusOK) | ||||
| 	unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID}) | ||||
| } | ||||
| 
 | ||||
| func TestIssueCommentUpdate(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 	session := loginUser(t, "user2") | ||||
| 	issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description") | ||||
| 	comment1 := "Test comment 1" | ||||
| 	commentID := testIssueAddComment(t, session, issueURL, comment1, "") | ||||
| 
 | ||||
| 	comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID}) | ||||
| 	assert.Equal(t, comment1, comment.Content) | ||||
| 
 | ||||
| 	modifiedContent := comment.Content + "MODIFIED" | ||||
| 
 | ||||
| 	// Using the ID of a comment that does not belong to the repository must fail
 | ||||
| 	req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{ | ||||
| 		"_csrf":   GetCSRF(t, session, issueURL), | ||||
| 		"content": modifiedContent, | ||||
| 	}) | ||||
| 	session.MakeRequest(t, req, http.StatusNotFound) | ||||
| 
 | ||||
| 	req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{ | ||||
| 		"_csrf":   GetCSRF(t, session, issueURL), | ||||
| 		"content": modifiedContent, | ||||
| 	}) | ||||
| 	session.MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 	comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID}) | ||||
| 	assert.Equal(t, modifiedContent, comment.Content) | ||||
| } | ||||
| 
 | ||||
| func TestIssueReaction(t *testing.T) { | ||||
| 	defer tests.PrepareTestEnv(t)() | ||||
| 	session := loginUser(t, "user2") | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ func TestMirrorPull(t *testing.T) { | |||
| 
 | ||||
| 	release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v0.2") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.NoError(t, release_service.DeleteReleaseByID(ctx, release.ID, user, true)) | ||||
| 	assert.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true)) | ||||
| 
 | ||||
| 	ok = mirror_service.SyncPullMirror(ctx, mirror.ID) | ||||
| 	assert.True(t, ok) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue