mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
Several handlers and model helpers skipped checks that their siblings already perform. - Only match principal keys when looking up SSH certificate principals - Return no attachments for an unsaved issue instead of every issue-less one - Reject issue and comment attachments when adding release attachments - Share one permission check for editing, deleting and reacting to issue content and for comment attachments between the web UI and the API, refusing them on locked issues for users who can't write - Reject issue edits, deletion and label changes in archived repositories via the API - Apply cross-origin protection to the repository topics endpoint - Return 404 for the repositories tab of individual users and respect profile visibility for the heatmap Fixes: https://github.com/go-gitea/gitea/issues/39226 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: bircni <bircni@icloud.com>
379 lines
9.7 KiB
Go
379 lines
9.7 KiB
Go
// Copyright 2016 The Gogs Authors. All rights reserved.
|
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"reflect"
|
|
|
|
issues_model "gitea.dev/models/issues"
|
|
api "gitea.dev/modules/structs"
|
|
"gitea.dev/modules/web"
|
|
"gitea.dev/services/context"
|
|
"gitea.dev/services/convert"
|
|
issue_service "gitea.dev/services/issue"
|
|
)
|
|
|
|
// ListIssueLabels list all the labels of an issue
|
|
func ListIssueLabels(ctx *context.APIContext) {
|
|
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/labels issue issueGetLabels
|
|
// ---
|
|
// summary: Get an issue's labels
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: index
|
|
// in: path
|
|
// description: index of the issue
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/LabelList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrIssueNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := issue.LoadAttributes(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToLabelList(issue.Labels, ctx.Repo.Repository, ctx.Repo.Owner))
|
|
}
|
|
|
|
// AddIssueLabels add labels for an issue
|
|
func AddIssueLabels(ctx *context.APIContext) {
|
|
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/labels issue issueAddLabel
|
|
// ---
|
|
// summary: Add a label to an issue
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: index
|
|
// in: path
|
|
// description: index of the issue
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/IssueLabelsOption"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/LabelList"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
form := web.GetForm[*api.IssueLabelsOption](ctx)
|
|
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if err = issue_service.AddLabels(ctx, issue, ctx.Doer, labels); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, ctx.Repo.Owner))
|
|
}
|
|
|
|
// DeleteIssueLabel delete a label for an issue
|
|
func DeleteIssueLabel(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id} issue issueRemoveLabel
|
|
// ---
|
|
// summary: Remove a label from an issue
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: index
|
|
// in: path
|
|
// description: index of the issue
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// - name: id
|
|
// in: path
|
|
// description: id of the label to remove
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrIssueNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
ctx.Status(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// the label must belong to this repo (or its owning org); otherwise a foreign label ID
|
|
// is rejected the same way as a nonexistent one, closing a cross-repo enumeration oracle
|
|
labelID := ctx.PathParamInt64("id")
|
|
label, err := issues_model.GetLabelInRepoOrOrgByID(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner.ID, ctx.Repo.Owner.IsOrganization(), labelID)
|
|
if err != nil {
|
|
ctx.APIErrorAuto(err)
|
|
return
|
|
}
|
|
|
|
if err := issue_service.RemoveLabel(ctx, issue, ctx.Doer, label); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ReplaceIssueLabels replace labels for an issue
|
|
func ReplaceIssueLabels(ctx *context.APIContext) {
|
|
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/labels issue issueReplaceLabels
|
|
// ---
|
|
// summary: Replace an issue's labels
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: index
|
|
// in: path
|
|
// description: index of the issue
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/IssueLabelsOption"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/LabelList"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
form := web.GetForm[*api.IssueLabelsOption](ctx)
|
|
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if err := issue_service.ReplaceLabels(ctx, issue, ctx.Doer, labels); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, ctx.Repo.Owner))
|
|
}
|
|
|
|
// ClearIssueLabels delete all the labels for an issue
|
|
func ClearIssueLabels(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/labels issue issueClearLabels
|
|
// ---
|
|
// summary: Remove all labels from an issue
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: repo
|
|
// in: path
|
|
// description: name of the repo
|
|
// type: string
|
|
// required: true
|
|
// - name: index
|
|
// in: path
|
|
// description: index of the issue
|
|
// type: integer
|
|
// format: int64
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrIssueNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
ctx.Status(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if err := issue_service.ClearLabels(ctx, issue, ctx.Doer); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) {
|
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrIssueNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return nil, nil, err
|
|
}
|
|
|
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
ctx.APIError(http.StatusForbidden, "write permission is required")
|
|
return nil, nil, errors.New("permission denied")
|
|
}
|
|
|
|
var (
|
|
labelIDs []int64
|
|
labelNames []string
|
|
)
|
|
for _, label := range form.Labels {
|
|
rv := reflect.ValueOf(label)
|
|
switch rv.Kind() {
|
|
case reflect.Float64:
|
|
labelIDs = append(labelIDs, int64(rv.Float()))
|
|
case reflect.String:
|
|
labelNames = append(labelNames, rv.String())
|
|
default:
|
|
ctx.APIError(http.StatusBadRequest, "a label must be an integer or a string")
|
|
return nil, nil, errors.New("invalid label")
|
|
}
|
|
}
|
|
if len(labelIDs) > 0 && len(labelNames) > 0 {
|
|
ctx.APIError(http.StatusBadRequest, "labels should be an array of strings or integers")
|
|
return nil, nil, errors.New("invalid labels")
|
|
}
|
|
if len(labelNames) > 0 {
|
|
repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil, err
|
|
}
|
|
labelIDs = append(labelIDs, repoLabelIDs...)
|
|
if ctx.Repo.Owner.IsOrganization() {
|
|
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil, err
|
|
}
|
|
labelIDs = append(labelIDs, orgLabelIDs...)
|
|
}
|
|
}
|
|
|
|
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
return issue, labels, err
|
|
}
|