Skip to content

Validate hex colors when creating/editing labels #34623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions modules/label/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"fmt"
"regexp"
"strings"
)
"sync"

// colorPattern is a regexp which can validate label color
var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
"code.gitea.io/gitea/modules/util"
)

// Label represents label information loaded from template
type Label struct {
Expand All @@ -21,6 +21,10 @@ type Label struct {
ExclusiveOrder int `yaml:"exclusive_order,omitempty"`
}

var colorPattern = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^#([\da-fA-F]{3}|[\da-fA-F]{6})$`)
})

// NormalizeColor normalizes a color string to a 6-character hex code
func NormalizeColor(color string) (string, error) {
// normalize case
Expand All @@ -31,8 +35,8 @@ func NormalizeColor(color string) (string, error) {
color = "#" + color
}

if !colorPattern.MatchString(color) {
return "", fmt.Errorf("bad color code: %s", color)
if !colorPattern().MatchString(color) {
return "", util.NewInvalidArgumentErrorf("invalid color: %s", color)
}

// convert 3-character shorthand into 6-character version
Expand Down
10 changes: 10 additions & 0 deletions modules/test/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

// RedirectURL returns the redirect URL of a http response.
// It also works for JSONRedirect: `{"redirect": "..."}`
// FIXME: it should separate the logic of checking from header and JSON body
func RedirectURL(resp http.ResponseWriter) string {
loc := resp.Header().Get("Location")
if loc != "" {
Expand All @@ -34,6 +35,15 @@ func RedirectURL(resp http.ResponseWriter) string {
return ""
}

func ParseJSONError(buf []byte) (ret struct {
ErrorMessage string `json:"errorMessage"`
RenderFormat string `json:"renderFormat"`
},
) {
_ = json.Unmarshal(buf, &ret)
return ret
}

func IsNormalPageCompleted(s string) bool {
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
}
Expand Down
2 changes: 1 addition & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1433,7 +1433,6 @@ commitstatus.success = Success
ext_issues = Access to External Issues
ext_issues.desc = Link to an external issue tracker.

projects = Projects
projects.desc = Manage issues and pulls in projects.
projects.description = Description (optional)
projects.description_placeholder = Description
Expand Down Expand Up @@ -1653,6 +1652,7 @@ issues.save = Save
issues.label_title = Name
issues.label_description = Description
issues.label_color = Color
issues.label_color_invalid = Invalid color
issues.label_exclusive = Exclusive
issues.label_archive = Archive Label
issues.label_archived_filter = Show archived labels
Expand Down
36 changes: 17 additions & 19 deletions routers/web/org/org_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
package org

import (
"net/http"
"errors"

"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
Expand All @@ -32,14 +34,8 @@ func RetrieveLabels(ctx *context.Context) {

// NewLabel create new label for organization
func NewLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsLabels"] = true
ctx.Data["PageIsOrgSettings"] = true

if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
form := shared_label.GetLabelEditForm(ctx)
if ctx.Written() {
return
}

Expand All @@ -55,20 +51,22 @@ func NewLabel(ctx *context.Context) {
ctx.ServerError("NewLabel", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
}

// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
form := shared_label.GetLabelEditForm(ctx)
if ctx.Written() {
return
}

l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
if err != nil {
switch {
case issues_model.IsErrOrgLabelNotExist(err):
ctx.HTTPError(http.StatusNotFound)
default:
ctx.ServerError("UpdateLabel", err)
}
if errors.Is(err, util.ErrNotExist) {
ctx.JSONErrorNotFound()
return
} else if err != nil {
ctx.ServerError("GetLabelInOrgByID", err)
return
}

Expand All @@ -82,7 +80,7 @@ func UpdateLabel(ctx *context.Context) {
ctx.ServerError("UpdateLabel", err)
return
}
ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
}

// DeleteLabel delete a label
Expand Down
36 changes: 18 additions & 18 deletions routers/web/repo/issue_label.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package repo

import (
"errors"
"net/http"

"code.gitea.io/gitea/models/db"
Expand All @@ -13,7 +14,9 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
Expand Down Expand Up @@ -100,13 +103,8 @@ func RetrieveLabelsForList(ctx *context.Context) {

// NewLabel create new label for repository
func NewLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsLabels"] = true

if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
form := shared_label.GetLabelEditForm(ctx)
if ctx.Written() {
return
}

Expand All @@ -122,34 +120,36 @@ func NewLabel(ctx *context.Context) {
ctx.ServerError("NewLabel", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}

// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
form := shared_label.GetLabelEditForm(ctx)
if ctx.Written() {
return
}

l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
if err != nil {
switch {
case issues_model.IsErrRepoLabelNotExist(err):
ctx.HTTPError(http.StatusNotFound)
default:
ctx.ServerError("UpdateLabel", err)
}
if errors.Is(err, util.ErrNotExist) {
ctx.JSONErrorNotFound()
return
} else if err != nil {
ctx.ServerError("GetLabelInRepoByID", err)
return
}

l.Name = form.Title
l.Exclusive = form.Exclusive
l.ExclusiveOrder = form.ExclusiveOrder
l.Description = form.Description
l.Color = form.Color

l.SetArchived(form.IsArchived)
if err := issues_model.UpdateLabel(ctx, l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}

// DeleteLabel delete a label
Expand Down
Loading