gitea/models/issue_label.go
zeripath 42b9b46ad2
Never add labels not from this repository or organisation and remove org labels on transfer (#14928)
* Never add labels not from this repository or organisation and remove org labels on transfer

Prevent the addition of labels from outside of the repository or
organisation and remove organisation labels on transfer.

Related #14908

* switch to use sql

* subquery alias

* once more around the merry go round

* fix api problem
2021-03-12 18:45:49 +01:00

784 lines
20 KiB
Go

// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"fmt"
"html/template"
"regexp"
"strconv"
"strings"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
"xorm.io/xorm"
)
// LabelColorPattern is a regexp witch can validate LabelColor
var LabelColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
// Label represents a label of repository for issues.
type Label struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
OrgID int64 `xorm:"INDEX"`
Name string
Description string
Color string `xorm:"VARCHAR(7)"`
NumIssues int
NumClosedIssues int
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
NumOpenIssues int `xorm:"-"`
NumOpenRepoIssues int64 `xorm:"-"`
IsChecked bool `xorm:"-"`
QueryString string `xorm:"-"`
IsSelected bool `xorm:"-"`
IsExcluded bool `xorm:"-"`
}
// GetLabelTemplateFile loads the label template file by given name,
// then parses and returns a list of name-color pairs and optionally description.
func GetLabelTemplateFile(name string) ([][3]string, error) {
data, err := GetRepoInitFile("label", name)
if err != nil {
return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %v", err)}
}
lines := strings.Split(string(data), "\n")
list := make([][3]string, 0, len(lines))
for i := 0; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])
if len(line) == 0 {
continue
}
parts := strings.SplitN(line, ";", 2)
fields := strings.SplitN(parts[0], " ", 2)
if len(fields) != 2 {
return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)}
}
color := strings.Trim(fields[0], " ")
if len(color) == 6 {
color = "#" + color
}
if !LabelColorPattern.MatchString(color) {
return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("bad HTML color code in line: %s", line)}
}
var description string
if len(parts) > 1 {
description = strings.TrimSpace(parts[1])
}
fields[1] = strings.TrimSpace(fields[1])
list = append(list, [3]string{fields[1], color, description})
}
return list, nil
}
// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
func (label *Label) CalOpenIssues() {
label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
}
// CalOpenOrgIssues calculates the open issues of a label for a specific repo
func (label *Label) CalOpenOrgIssues(repoID, labelID int64) {
repoIDs := []int64{repoID}
labelIDs := []int64{labelID}
counts, _ := CountIssuesByRepo(&IssuesOptions{
RepoIDs: repoIDs,
LabelIDs: labelIDs,
})
for _, count := range counts {
label.NumOpenRepoIssues += count
}
}
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) {
var labelQuerySlice []string
labelSelected := false
labelID := strconv.FormatInt(label.ID, 10)
for _, s := range currentSelectedLabels {
if s == label.ID {
labelSelected = true
} else if -s == label.ID {
labelSelected = true
label.IsExcluded = true
} else if s != 0 {
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
}
}
if !labelSelected {
labelQuerySlice = append(labelQuerySlice, labelID)
}
label.IsSelected = labelSelected
label.QueryString = strings.Join(labelQuerySlice, ",")
}
// BelongsToOrg returns true if label is an organization label
func (label *Label) BelongsToOrg() bool {
return label.OrgID > 0
}
// BelongsToRepo returns true if label is a repository label
func (label *Label) BelongsToRepo() bool {
return label.RepoID > 0
}
// ForegroundColor calculates the text color for labels based
// on their background color.
func (label *Label) ForegroundColor() template.CSS {
if strings.HasPrefix(label.Color, "#") {
if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
r := float32(0xFF & (color >> 16))
g := float32(0xFF & (color >> 8))
b := float32(0xFF & color)
luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
if luminance < 0.66 {
return template.CSS("#fff")
}
}
}
// default to black
return template.CSS("#000")
}
// .____ ___. .__
// | | _____ \_ |__ ____ | |
// | | \__ \ | __ \_/ __ \| |
// | |___ / __ \| \_\ \ ___/| |__
// >_______ (____ /___ /\___ >____/
func loadLabels(labelTemplate string) ([]string, error) {
list, err := GetLabelTemplateFile(labelTemplate)
if err != nil {
return nil, err
}
labels := make([]string, len(list))
for i := 0; i < len(list); i++ {
labels[i] = list[i][0]
}
return labels, nil
}
// LoadLabelsFormatted loads the labels' list of a template file as a string separated by comma
func LoadLabelsFormatted(labelTemplate string) (string, error) {
labels, err := loadLabels(labelTemplate)
return strings.Join(labels, ", "), err
}
func initializeLabels(e Engine, id int64, labelTemplate string, isOrg bool) error {
list, err := GetLabelTemplateFile(labelTemplate)
if err != nil {
return err
}
labels := make([]*Label, len(list))
for i := 0; i < len(list); i++ {
labels[i] = &Label{
Name: list[i][0],
Description: list[i][2],
Color: list[i][1],
}
if isOrg {
labels[i].OrgID = id
} else {
labels[i].RepoID = id
}
}
for _, label := range labels {
if err = newLabel(e, label); err != nil {
return err
}
}
return nil
}
// InitializeLabels adds a label set to a repository using a template
func InitializeLabels(ctx DBContext, repoID int64, labelTemplate string, isOrg bool) error {
return initializeLabels(ctx.e, repoID, labelTemplate, isOrg)
}
func newLabel(e Engine, label *Label) error {
_, err := e.Insert(label)
return err
}
// NewLabel creates a new label
func NewLabel(label *Label) error {
if !LabelColorPattern.MatchString(label.Color) {
return fmt.Errorf("bad color code: %s", label.Color)
}
return newLabel(x, label)
}
// NewLabels creates new labels
func NewLabels(labels ...*Label) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
for _, label := range labels {
if !LabelColorPattern.MatchString(label.Color) {
return fmt.Errorf("bad color code: %s", label.Color)
}
if err := newLabel(sess, label); err != nil {
return err
}
}
return sess.Commit()
}
// UpdateLabel updates label information.
func UpdateLabel(l *Label) error {
if !LabelColorPattern.MatchString(l.Color) {
return fmt.Errorf("bad color code: %s", l.Color)
}
return updateLabelCols(x, l, "name", "description", "color")
}
// DeleteLabel delete a label
func DeleteLabel(id, labelID int64) error {
label, err := GetLabelByID(labelID)
if err != nil {
if IsErrLabelNotExist(err) {
return nil
}
return err
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if label.BelongsToOrg() && label.OrgID != id {
return nil
}
if label.BelongsToRepo() && label.RepoID != id {
return nil
}
if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
return err
} else if _, err = sess.
Where("label_id = ?", labelID).
Delete(new(IssueLabel)); err != nil {
return err
}
// delete comments about now deleted label_id
if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil {
return err
}
return sess.Commit()
}
// getLabelByID returns a label by label id
func getLabelByID(e Engine, labelID int64) (*Label, error) {
if labelID <= 0 {
return nil, ErrLabelNotExist{labelID}
}
l := &Label{}
has, err := e.ID(labelID).Get(l)
if err != nil {
return nil, err
} else if !has {
return nil, ErrLabelNotExist{l.ID}
}
return l, nil
}
// GetLabelByID returns a label by given ID.
func GetLabelByID(id int64) (*Label, error) {
return getLabelByID(x, id)
}
// GetLabelsByIDs returns a list of labels by IDs
func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) {
labels := make([]*Label, 0, len(labelIDs))
return labels, x.Table("label").
In("id", labelIDs).
Asc("name").
Cols("id", "repo_id", "org_id").
Find(&labels)
}
// __________ .__ __
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
// \/ \/|__| \/ \/
// getLabelInRepoByName returns a label by Name in given repository.
func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, error) {
if len(labelName) == 0 || repoID <= 0 {
return nil, ErrRepoLabelNotExist{0, repoID}
}
l := &Label{
Name: labelName,
RepoID: repoID,
}
has, err := e.Get(l)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRepoLabelNotExist{0, l.RepoID}
}
return l, nil
}
// getLabelInRepoByID returns a label by ID in given repository.
func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
if labelID <= 0 || repoID <= 0 {
return nil, ErrRepoLabelNotExist{labelID, repoID}
}
l := &Label{
ID: labelID,
RepoID: repoID,
}
has, err := e.Get(l)
if err != nil {
return nil, err
} else if !has {
return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
}
return l, nil
}
// GetLabelInRepoByName returns a label by name in given repository.
func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) {
return getLabelInRepoByName(x, repoID, labelName)
}
// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
// repository.
// it silently ignores label names that do not belong to the repository.
func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) {
labelIDs := make([]int64, 0, len(labelNames))
return labelIDs, x.Table("label").
Where("repo_id = ?", repoID).
In("name", labelNames).
Asc("name").
Cols("id").
Find(&labelIDs)
}
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
return builder.Select("issue_label.issue_id").
From("issue_label").
InnerJoin("label", "label.id = issue_label.label_id").
Where(
builder.In("label.name", labelNames),
).
GroupBy("issue_label.issue_id")
}
// GetLabelInRepoByID returns a label by ID in given repository.
func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) {
return getLabelInRepoByID(x, repoID, labelID)
}
// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
// it silently ignores label IDs that do not belong to the repository.
func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
labels := make([]*Label, 0, len(labelIDs))
return labels, x.
Where("repo_id = ?", repoID).
In("id", labelIDs).
Asc("name").
Find(&labels)
}
func getLabelsByRepoID(e Engine, repoID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
if repoID <= 0 {
return nil, ErrRepoLabelNotExist{0, repoID}
}
labels := make([]*Label, 0, 10)
sess := e.Where("repo_id = ?", repoID)
switch sortType {
case "reversealphabetically":
sess.Desc("name")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
default:
sess.Asc("name")
}
if listOptions.Page != 0 {
sess = listOptions.setSessionPagination(sess)
}
return labels, sess.Find(&labels)
}
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
func GetLabelsByRepoID(repoID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
return getLabelsByRepoID(x, repoID, sortType, listOptions)
}
// ________
// \_____ \_______ ____
// / | \_ __ \/ ___\
// / | \ | \/ /_/ >
// \_______ /__| \___ /
// \/ /_____/
// getLabelInOrgByName returns a label by Name in given organization
func getLabelInOrgByName(e Engine, orgID int64, labelName string) (*Label, error) {
if len(labelName) == 0 || orgID <= 0 {
return nil, ErrOrgLabelNotExist{0, orgID}
}
l := &Label{
Name: labelName,
OrgID: orgID,
}
has, err := e.Get(l)
if err != nil {
return nil, err
} else if !has {
return nil, ErrOrgLabelNotExist{0, l.OrgID}
}
return l, nil
}
// getLabelInOrgByID returns a label by ID in given organization.
func getLabelInOrgByID(e Engine, orgID, labelID int64) (*Label, error) {
if labelID <= 0 || orgID <= 0 {
return nil, ErrOrgLabelNotExist{labelID, orgID}
}
l := &Label{
ID: labelID,
OrgID: orgID,
}
has, err := e.Get(l)
if err != nil {
return nil, err
} else if !has {
return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
}
return l, nil
}
// GetLabelInOrgByName returns a label by name in given organization.
func GetLabelInOrgByName(orgID int64, labelName string) (*Label, error) {
return getLabelInOrgByName(x, orgID, labelName)
}
// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given
// organization.
func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) {
if orgID <= 0 {
return nil, ErrOrgLabelNotExist{0, orgID}
}
labelIDs := make([]int64, 0, len(labelNames))
return labelIDs, x.Table("label").
Where("org_id = ?", orgID).
In("name", labelNames).
Asc("name").
Cols("id").
Find(&labelIDs)
}
// GetLabelIDsInOrgsByNames returns a list of labelIDs by names in one of the given
// organization.
// it silently ignores label names that do not belong to the organization.
func GetLabelIDsInOrgsByNames(orgIDs []int64, labelNames []string) ([]int64, error) {
labelIDs := make([]int64, 0, len(labelNames))
return labelIDs, x.Table("label").
In("org_id", orgIDs).
In("name", labelNames).
Asc("name").
Cols("id").
Find(&labelIDs)
}
// GetLabelInOrgByID returns a label by ID in given organization.
func GetLabelInOrgByID(orgID, labelID int64) (*Label, error) {
return getLabelInOrgByID(x, orgID, labelID)
}
// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
// it silently ignores label IDs that do not belong to the organization.
func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) {
labels := make([]*Label, 0, len(labelIDs))
return labels, x.
Where("org_id = ?", orgID).
In("id", labelIDs).
Asc("name").
Find(&labels)
}
func getLabelsByOrgID(e Engine, orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
if orgID <= 0 {
return nil, ErrOrgLabelNotExist{0, orgID}
}
labels := make([]*Label, 0, 10)
sess := e.Where("org_id = ?", orgID)
switch sortType {
case "reversealphabetically":
sess.Desc("name")
case "leastissues":
sess.Asc("num_issues")
case "mostissues":
sess.Desc("num_issues")
default:
sess.Asc("name")
}
if listOptions.Page != 0 {
sess = listOptions.setSessionPagination(sess)
}
return labels, sess.Find(&labels)
}
// GetLabelsByOrgID returns all labels that belong to given organization by ID.
func GetLabelsByOrgID(orgID int64, sortType string, listOptions ListOptions) ([]*Label, error) {
return getLabelsByOrgID(x, orgID, sortType, listOptions)
}
// .___
// | | ______ ________ __ ____
// | |/ ___// ___/ | \_/ __ \
// | |\___ \ \___ \| | /\ ___/
// |___/____ >____ >____/ \___ |
// \/ \/ \/
func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
var labels []*Label
return labels, e.Where("issue_label.issue_id = ?", issueID).
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
Asc("label.name").
Find(&labels)
}
// GetLabelsByIssueID returns all labels that belong to given issue by ID.
func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
return getLabelsByIssueID(x, issueID)
}
func updateLabelCols(e Engine, l *Label, cols ...string) error {
_, err := e.ID(l.ID).
SetExpr("num_issues",
builder.Select("count(*)").From("issue_label").
Where(builder.Eq{"label_id": l.ID}),
).
SetExpr("num_closed_issues",
builder.Select("count(*)").From("issue_label").
InnerJoin("issue", "issue_label.issue_id = issue.id").
Where(builder.Eq{
"issue_label.label_id": l.ID,
"issue.is_closed": true,
}),
).
Cols(cols...).Update(l)
return err
}
// .___ .____ ___. .__
// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
// \/ \/ \/ \/ \/ \/ \/
// IssueLabel represents an issue-label relation.
type IssueLabel struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"UNIQUE(s)"`
LabelID int64 `xorm:"UNIQUE(s)"`
}
func hasIssueLabel(e Engine, issueID, labelID int64) bool {
has, _ := e.Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
return has
}
// HasIssueLabel returns true if issue has been labeled.
func HasIssueLabel(issueID, labelID int64) bool {
return hasIssueLabel(x, issueID, labelID)
}
// newIssueLabel this function creates a new label it does not check if the label is valid for the issue
// YOU MUST CHECK THIS BEFORE THIS FUNCTION
func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
if _, err = e.Insert(&IssueLabel{
IssueID: issue.ID,
LabelID: label.ID,
}); err != nil {
return err
}
if err = issue.loadRepo(e); err != nil {
return
}
var opts = &CreateCommentOptions{
Type: CommentTypeLabel,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
Label: label,
Content: "1",
}
if _, err = createComment(e, opts); err != nil {
return err
}
return updateLabelCols(e, label, "num_issues", "num_closed_issue")
}
// NewIssueLabel creates a new issue-label relation.
func NewIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
if HasIssueLabel(issue.ID, label.ID) {
return nil
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if err = issue.loadRepo(sess); err != nil {
return err
}
// Do NOT add invalid labels
if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID {
return nil
}
if err = newIssueLabel(sess, issue, label, doer); err != nil {
return err
}
issue.Labels = nil
if err = issue.loadLabels(sess); err != nil {
return err
}
return sess.Commit()
}
// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label, doer *User) (err error) {
if err = issue.loadRepo(e); err != nil {
return err
}
for _, label := range labels {
// Don't add already present labels and invalid labels
if hasIssueLabel(e, issue.ID, label.ID) ||
(label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) {
continue
}
if err = newIssueLabel(e, issue, label, doer); err != nil {
return fmt.Errorf("newIssueLabel: %v", err)
}
}
return nil
}
// NewIssueLabels creates a list of issue-label relations.
func NewIssueLabels(issue *Issue, labels []*Label, doer *User) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if err = newIssueLabels(sess, issue, labels, doer); err != nil {
return err
}
issue.Labels = nil
if err = issue.loadLabels(sess); err != nil {
return err
}
return sess.Commit()
}
func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
if count, err := e.Delete(&IssueLabel{
IssueID: issue.ID,
LabelID: label.ID,
}); err != nil {
return err
} else if count == 0 {
return nil
}
if err = issue.loadRepo(e); err != nil {
return
}
var opts = &CreateCommentOptions{
Type: CommentTypeLabel,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
Label: label,
}
if _, err = createComment(e, opts); err != nil {
return err
}
return updateLabelCols(e, label, "num_issues", "num_closed_issue")
}
// DeleteIssueLabel deletes issue-label relation.
func DeleteIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if err = deleteIssueLabel(sess, issue, label, doer); err != nil {
return err
}
issue.Labels = nil
if err = issue.loadLabels(sess); err != nil {
return err
}
return sess.Commit()
}