Skip to content
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

Improve Gitea's web context, decouple "issue template" code into service package #24590

Merged
merged 4 commits into from
May 8, 2023
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
23 changes: 12 additions & 11 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,20 @@ type Render interface {

// Context represents context of a request.
type Context struct {
Resp ResponseWriter
Req *http.Request
Resp ResponseWriter
Req *http.Request
Render Render

Data middleware.ContextData // data used by MVC templates
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
Render Render
Locale translation.Locale
Cache cache.Cache
Csrf CSRFProtector
Flash *middleware.Flash
Session session.Store

Link string // current request URL
EscapedLink string

Locale translation.Locale
Cache cache.Cache
Csrf CSRFProtector
Flash *middleware.Flash
Session session.Store

Link string // current request URL (without query string)
Doer *user_model.User
IsSigned bool
IsBasicAuth bool
Expand Down
19 changes: 0 additions & 19 deletions modules/context/context_cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package context
import (
"encoding/hex"
"net/http"
"strconv"
"strings"

"code.gitea.io/gitea/modules/setting"
Expand Down Expand Up @@ -85,21 +84,3 @@ func (ctx *Context) CookieEncrypt(secret, value string) string {

return hex.EncodeToString(text)
}

// GetCookieInt returns cookie result in int type.
func (ctx *Context) GetCookieInt(name string) int {
r, _ := strconv.Atoi(ctx.GetSiteCookie(name))
return r
}

// GetCookieInt64 returns cookie result in int64 type.
func (ctx *Context) GetCookieInt64(name string) int64 {
r, _ := strconv.ParseInt(ctx.GetSiteCookie(name), 10, 64)
return r
}

// GetCookieFloat64 returns cookie result in float64 type.
func (ctx *Context) GetCookieFloat64(name string) float64 {
v, _ := strconv.ParseFloat(ctx.GetSiteCookie(name), 64)
return v
}
109 changes: 0 additions & 109 deletions modules/context/context_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,14 @@
package context

import (
"path"
"strings"

"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)

// IsUserSiteAdmin returns true if current user is a site admin
func (ctx *Context) IsUserSiteAdmin() bool {
return ctx.IsSigned && ctx.Doer.IsAdmin
}

// IsUserRepoOwner returns true if current user owns current repo
func (ctx *Context) IsUserRepoOwner() bool {
return ctx.Repo.IsOwner()
}

// IsUserRepoAdmin returns true if current user is admin in current repo
func (ctx *Context) IsUserRepoAdmin() bool {
return ctx.Repo.IsAdmin()
Expand All @@ -39,100 +27,3 @@ func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool {

return false
}

// IsUserRepoReaderSpecific returns true if current user can read current repo's specific part
func (ctx *Context) IsUserRepoReaderSpecific(unitType unit.Type) bool {
return ctx.Repo.CanRead(unitType)
}

// IsUserRepoReaderAny returns true if current user can read any part of current repo
func (ctx *Context) IsUserRepoReaderAny() bool {
return ctx.Repo.HasAccess()
}

// IssueTemplatesFromDefaultBranch checks for valid issue templates in the repo's default branch,
func (ctx *Context) IssueTemplatesFromDefaultBranch() []*api.IssueTemplate {
ret, _ := ctx.IssueTemplatesErrorsFromDefaultBranch()
return ret
}

// IssueTemplatesErrorsFromDefaultBranch checks for issue templates in the repo's default branch,
// returns valid templates and the errors of invalid template files.
func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplate, map[string]error) {
var issueTemplates []*api.IssueTemplate

if ctx.Repo.Repository.IsEmpty {
return issueTemplates, nil
}

if ctx.Repo.Commit == nil {
var err error
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return issueTemplates, nil
}
}

invalidFiles := map[string]error{}
for _, dirName := range IssueTemplateDirCandidates {
tree, err := ctx.Repo.Commit.SubTree(dirName)
if err != nil {
log.Debug("get sub tree of %s: %v", dirName, err)
continue
}
entries, err := tree.ListEntries()
if err != nil {
log.Debug("list entries in %s: %v", dirName, err)
return issueTemplates, nil
}
for _, entry := range entries {
if !template.CouldBe(entry.Name()) {
continue
}
fullName := path.Join(dirName, entry.Name())
if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
invalidFiles[fullName] = err
} else {
if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
it.Ref = git.BranchPrefix + it.Ref
}
issueTemplates = append(issueTemplates, it)
}
}
}
return issueTemplates, invalidFiles
}

// IssueConfigFromDefaultBranch returns the issue config for this repo.
// It never returns a nil config.
func (ctx *Context) IssueConfigFromDefaultBranch() (api.IssueConfig, error) {
if ctx.Repo.Repository.IsEmpty {
return GetDefaultIssueConfig(), nil
}

commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return GetDefaultIssueConfig(), err
}

for _, configName := range IssueConfigCandidates {
if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
return ctx.Repo.GetIssueConfig(configName+".yaml", commit)
}

if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
return ctx.Repo.GetIssueConfig(configName+".yml", commit)
}
}

return GetDefaultIssueConfig(), nil
}

func (ctx *Context) HasIssueTemplatesOrContactLinks() bool {
if len(ctx.IssueTemplatesFromDefaultBranch()) > 0 {
return true
}

issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
return len(issueConfig.ContactLinks) > 0
}
93 changes: 0 additions & 93 deletions modules/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"context"
"fmt"
"html"
"io"
"net/http"
"net/url"
"path"
Expand All @@ -28,33 +27,12 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"

"github.com/editorconfig/editorconfig-core-go/v2"
"gopkg.in/yaml.v3"
)

// IssueTemplateDirCandidates issue templates directory
var IssueTemplateDirCandidates = []string{
"ISSUE_TEMPLATE",
"issue_template",
".gitea/ISSUE_TEMPLATE",
".gitea/issue_template",
".github/ISSUE_TEMPLATE",
".github/issue_template",
".gitlab/ISSUE_TEMPLATE",
".gitlab/issue_template",
}

var IssueConfigCandidates = []string{
".gitea/ISSUE_TEMPLATE/config",
".gitea/issue_template/config",
".github/ISSUE_TEMPLATE/config",
".github/issue_template/config",
}

// PullRequest contains information to make a pull request
type PullRequest struct {
BaseRepo *repo_model.Repository
Expand Down Expand Up @@ -1061,74 +1039,3 @@ func UnitTypes() func(ctx *Context) {
ctx.Data["UnitTypeActions"] = unit_model.TypeActions
}
}

func GetDefaultIssueConfig() api.IssueConfig {
return api.IssueConfig{
BlankIssuesEnabled: true,
ContactLinks: make([]api.IssueConfigContactLink, 0),
}
}

// GetIssueConfig loads the given issue config file.
// It never returns a nil config.
func (r *Repository) GetIssueConfig(path string, commit *git.Commit) (api.IssueConfig, error) {
if r.GitRepo == nil {
return GetDefaultIssueConfig(), nil
}

var err error

treeEntry, err := commit.GetTreeEntryByPath(path)
if err != nil {
return GetDefaultIssueConfig(), err
}

reader, err := treeEntry.Blob().DataAsync()
if err != nil {
log.Debug("DataAsync: %v", err)
return GetDefaultIssueConfig(), nil
}

defer reader.Close()

configContent, err := io.ReadAll(reader)
if err != nil {
return GetDefaultIssueConfig(), err
}

issueConfig := api.IssueConfig{}
if err := yaml.Unmarshal(configContent, &issueConfig); err != nil {
return GetDefaultIssueConfig(), err
}

for pos, link := range issueConfig.ContactLinks {
if link.Name == "" {
return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1)
}

if link.URL == "" {
return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1)
}

if link.About == "" {
return GetDefaultIssueConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1)
}

_, err = url.ParseRequestURI(link.URL)
if err != nil {
return GetDefaultIssueConfig(), fmt.Errorf("%s is not a valid URL", link.URL)
}
}

return issueConfig, nil
}

// IsIssueConfig returns if the given path is a issue config file.
func (r *Repository) IsIssueConfig(path string) bool {
for _, configName := range IssueConfigCandidates {
if path == configName+".yaml" || path == configName+".yml" {
return true
}
}
return false
}
6 changes: 3 additions & 3 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) {
// reqOwner user should be the owner of the repo or site admin.
func reqOwner() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoOwner() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo")
return
}
Expand Down Expand Up @@ -355,7 +355,7 @@ func reqRepoBranchWriter(ctx *context.APIContext) {
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoReaderSpecific(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin")
return
}
Expand All @@ -365,7 +365,7 @@ func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
// reqAnyRepoReader user should have any permission to read repository or permissions of site admin
func reqAnyRepoReader() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoReaderAny() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() {
ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin")
return
}
Expand Down
13 changes: 9 additions & 4 deletions routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
)

Expand Down Expand Up @@ -1144,8 +1145,12 @@ func GetIssueTemplates(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/IssueTemplates"

ctx.JSON(http.StatusOK, ctx.IssueTemplatesFromDefaultBranch())
ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err)
return
}
ctx.JSON(http.StatusOK, ret)
}

// GetIssueConfig returns the issue config for a repo
Expand All @@ -1169,7 +1174,7 @@ func GetIssueConfig(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/RepoIssueConfig"
issueConfig, _ := ctx.IssueConfigFromDefaultBranch()
issueConfig, _ := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.JSON(http.StatusOK, issueConfig)
}

Expand All @@ -1194,7 +1199,7 @@ func ValidateIssueConfig(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/RepoIssueConfigValidation"
_, err := ctx.IssueConfigFromDefaultBranch()
_, err := issue.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)

if err == nil {
ctx.JSON(http.StatusOK, api.IssueConfigValidation{Valid: true, Message: ""})
Expand Down
Loading