mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-20 03:43:10 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
@@ -337,12 +337,13 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
|
||||
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Repo.Commit = commit
|
||||
ctx.Repo.TreePath = ctx.Params("*")
|
||||
next.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -446,6 +446,17 @@ func (ctx *Context) JSON(status int, content interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func removeSessionCookieHeader(w http.ResponseWriter) {
|
||||
cookies := w.Header()["Set-Cookie"]
|
||||
w.Header().Del("Set-Cookie")
|
||||
for _, cookie := range cookies {
|
||||
if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
|
||||
continue
|
||||
}
|
||||
w.Header().Add("Set-Cookie", cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect redirects the request
|
||||
func (ctx *Context) Redirect(location string, status ...int) {
|
||||
code := http.StatusSeeOther
|
||||
@@ -453,6 +464,15 @@ func (ctx *Context) Redirect(location string, status ...int) {
|
||||
code = status[0]
|
||||
}
|
||||
|
||||
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
|
||||
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
|
||||
// 1. the first request to "/my-path" contains cookie
|
||||
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
|
||||
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
|
||||
// 4. then the browser accepts the empty session, then the user is logged out
|
||||
// So in this case, we should remove the session cookie from the response header
|
||||
removeSessionCookieHeader(ctx.Resp)
|
||||
}
|
||||
http.Redirect(ctx.Resp, ctx.Req, location, code)
|
||||
}
|
||||
|
||||
@@ -657,7 +677,7 @@ func getCsrfOpts() CsrfOptions {
|
||||
|
||||
// Contexter initializes a classic context for a request.
|
||||
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
|
||||
_, rnd := templates.HTMLRenderer(ctx)
|
||||
rnd := templates.HTMLRenderer()
|
||||
csrfOpts := getCsrfOpts()
|
||||
if !setting.IsProd {
|
||||
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockResponseWriter struct {
|
||||
header http.Header
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Header() http.Header {
|
||||
return m.header
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Write(bytes []byte) (int, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) WriteHeader(statusCode int) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func TestRemoveSessionCookieHeader(t *testing.T) {
|
||||
w := &mockResponseWriter{}
|
||||
w.header = http.Header{}
|
||||
w.header.Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
|
||||
w.header.Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
|
||||
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
|
||||
removeSessionCookieHeader(w)
|
||||
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
|
||||
assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func determineAccessMode(ctx *Context) (perm.AccessMode, error) {
|
||||
|
||||
// PackageContexter initializes a package context for a request.
|
||||
func PackageContexter(ctx gocontext.Context) func(next http.Handler) http.Handler {
|
||||
_, rnd := templates.HTMLRenderer(ctx)
|
||||
rnd := templates.HTMLRenderer()
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
ctx := Context{
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
@@ -106,3 +110,32 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoScopedToken check whether personal access token has repo scope
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) {
|
||||
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if ok { // it's a personal access token but not oauth2 token
|
||||
var scopeMatched bool
|
||||
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
if !scopeMatched && !repo.IsPrivate {
|
||||
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !scopeMatched {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +187,8 @@ func (c *Commit) CommitsCount() (int64, error) {
|
||||
}
|
||||
|
||||
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
|
||||
func (c *Commit) CommitsByRange(page, pageSize int) ([]*Commit, error) {
|
||||
return c.repo.commitsByRange(c.ID, page, pageSize)
|
||||
func (c *Commit) CommitsByRange(page, pageSize int, not string) ([]*Commit, error) {
|
||||
return c.repo.commitsByRange(c.ID, page, pageSize, not)
|
||||
}
|
||||
|
||||
// CommitsBefore returns all the commits before current revision
|
||||
|
||||
@@ -90,14 +90,22 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
|
||||
return commits[0], nil
|
||||
}
|
||||
|
||||
func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) ([]*Commit, error) {
|
||||
stdout, _, err := NewCommand(repo.Ctx, "log").
|
||||
AddOptionFormat("--skip=%d", (page-1)*pageSize).AddOptionFormat("--max-count=%d", pageSize).AddArguments(prettyLogFormat).
|
||||
AddDynamicArguments(id.String()).
|
||||
RunStdBytes(&RunOpts{Dir: repo.Path})
|
||||
func (repo *Repository) commitsByRange(id SHA1, page, pageSize int, not string) ([]*Commit, error) {
|
||||
cmd := NewCommand(repo.Ctx, "log").
|
||||
AddOptionFormat("--skip=%d", (page-1)*pageSize).
|
||||
AddOptionFormat("--max-count=%d", pageSize).
|
||||
AddArguments(prettyLogFormat).
|
||||
AddDynamicArguments(id.String())
|
||||
|
||||
if not != "" {
|
||||
cmd.AddOptionValues("--not", not)
|
||||
}
|
||||
|
||||
stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repo.parsePrettyFormatLogToList(stdout)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
"github.com/blakesmith/ar"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/ulikunitz/xz"
|
||||
)
|
||||
|
||||
const (
|
||||
PropertyDistribution = "debian.distribution"
|
||||
PropertyComponent = "debian.component"
|
||||
PropertyArchitecture = "debian.architecture"
|
||||
PropertyControl = "debian.control"
|
||||
PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release"
|
||||
|
||||
SettingKeyPrivate = "debian.key.private"
|
||||
SettingKeyPublic = "debian.key.public"
|
||||
|
||||
RepositoryPackage = "_debian"
|
||||
RepositoryVersion = "_repository"
|
||||
|
||||
controlTar = "control.tar"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing")
|
||||
ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm")
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
|
||||
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source
|
||||
namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`)
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
|
||||
versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
Name string
|
||||
Version string
|
||||
Architecture string
|
||||
Control string
|
||||
Metadata *Metadata
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Maintainer string `json:"maintainer,omitempty"`
|
||||
ProjectURL string `json:"project_url,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
// ParsePackage parses the Debian package file
|
||||
// https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html
|
||||
func ParsePackage(r io.Reader) (*Package, error) {
|
||||
arr := ar.NewReader(r)
|
||||
|
||||
for {
|
||||
hd, err := arr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(hd.Name, controlTar) {
|
||||
var inner io.Reader
|
||||
switch hd.Name[len(controlTar):] {
|
||||
case "":
|
||||
inner = arr
|
||||
case ".gz":
|
||||
gzr, err := gzip.NewReader(arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
inner = gzr
|
||||
case ".xz":
|
||||
xzr, err := xz.NewReader(arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inner = xzr
|
||||
case ".zst":
|
||||
zr, err := zstd.NewReader(arr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
inner = zr
|
||||
default:
|
||||
return nil, ErrUnsupportedCompression
|
||||
}
|
||||
|
||||
tr := tar.NewReader(inner)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hd.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
if hd.FileInfo().Name() == "control" {
|
||||
return ParseControlFile(tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMissingControlFile
|
||||
}
|
||||
|
||||
// ParseControlFile parses a Debian control file to retrieve the metadata
|
||||
func ParseControlFile(r io.Reader) (*Package, error) {
|
||||
p := &Package{
|
||||
Metadata: &Metadata{},
|
||||
}
|
||||
|
||||
key := ""
|
||||
var depends strings.Builder
|
||||
var control strings.Builder
|
||||
|
||||
s := bufio.NewScanner(io.TeeReader(r, &control))
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if line[0] == ' ' || line[0] == '\t' {
|
||||
switch key {
|
||||
case "Description":
|
||||
p.Metadata.Description += line
|
||||
case "Depends":
|
||||
depends.WriteString(trimmed)
|
||||
}
|
||||
} else {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key = parts[0]
|
||||
value := strings.TrimSpace(parts[1])
|
||||
switch key {
|
||||
case "Package":
|
||||
if !namePattern.MatchString(value) {
|
||||
return nil, ErrInvalidName
|
||||
}
|
||||
p.Name = value
|
||||
case "Version":
|
||||
if !versionPattern.MatchString(value) {
|
||||
return nil, ErrInvalidVersion
|
||||
}
|
||||
p.Version = value
|
||||
case "Architecture":
|
||||
if value == "" {
|
||||
return nil, ErrInvalidArchitecture
|
||||
}
|
||||
p.Architecture = value
|
||||
case "Maintainer":
|
||||
a, err := mail.ParseAddress(value)
|
||||
if err != nil || a.Name == "" {
|
||||
p.Metadata.Maintainer = value
|
||||
} else {
|
||||
p.Metadata.Maintainer = a.Name
|
||||
}
|
||||
case "Description":
|
||||
p.Metadata.Description = value
|
||||
case "Depends":
|
||||
depends.WriteString(value)
|
||||
case "Homepage":
|
||||
if validation.IsValidURL(value) {
|
||||
p.Metadata.ProjectURL = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dependencies := strings.Split(depends.String(), ",")
|
||||
for i := range dependencies {
|
||||
dependencies[i] = strings.TrimSpace(dependencies[i])
|
||||
}
|
||||
p.Metadata.Dependencies = dependencies
|
||||
|
||||
p.Control = control.String()
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/blakesmith/ar"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/ulikunitz/xz"
|
||||
)
|
||||
|
||||
const (
|
||||
packageName = "gitea"
|
||||
packageVersion = "0:1.0.1-te~st"
|
||||
packageArchitecture = "amd64"
|
||||
packageAuthor = "KN4CK3R"
|
||||
description = "Description with multiple lines."
|
||||
projectURL = "https://gitea.io"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
createArchive := func(files map[string][]byte) io.Reader {
|
||||
var buf bytes.Buffer
|
||||
aw := ar.NewWriter(&buf)
|
||||
aw.WriteGlobalHeader()
|
||||
for filename, content := range files {
|
||||
hdr := &ar.Header{
|
||||
Name: filename,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
aw.WriteHeader(hdr)
|
||||
aw.Write(content)
|
||||
}
|
||||
return &buf
|
||||
}
|
||||
|
||||
t.Run("MissingControlFile", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{"dummy.txt": {}})
|
||||
|
||||
p, err := ParsePackage(data)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrMissingControlFile)
|
||||
})
|
||||
|
||||
t.Run("Compression", func(t *testing.T) {
|
||||
t.Run("Unsupported", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{"control.tar.foo": {}})
|
||||
|
||||
p, err := ParsePackage(data)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrUnsupportedCompression)
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
tw.WriteHeader(&tar.Header{
|
||||
Name: "control",
|
||||
Mode: 0o600,
|
||||
Size: 50,
|
||||
})
|
||||
tw.Write([]byte("Package: gitea\nVersion: 1.0.0\nArchitecture: amd64\n"))
|
||||
tw.Close()
|
||||
|
||||
t.Run("None", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{"control.tar": buf.Bytes()})
|
||||
|
||||
p, err := ParsePackage(data)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "gitea", p.Name)
|
||||
})
|
||||
|
||||
t.Run("gz", func(t *testing.T) {
|
||||
var zbuf bytes.Buffer
|
||||
zw := gzip.NewWriter(&zbuf)
|
||||
zw.Write(buf.Bytes())
|
||||
zw.Close()
|
||||
|
||||
data := createArchive(map[string][]byte{"control.tar.gz": zbuf.Bytes()})
|
||||
|
||||
p, err := ParsePackage(data)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "gitea", p.Name)
|
||||
})
|
||||
|
||||
t.Run("xz", func(t *testing.T) {
|
||||
var xbuf bytes.Buffer
|
||||
xw, _ := xz.NewWriter(&xbuf)
|
||||
xw.Write(buf.Bytes())
|
||||
xw.Close()
|
||||
|
||||
data := createArchive(map[string][]byte{"control.tar.xz": xbuf.Bytes()})
|
||||
|
||||
p, err := ParsePackage(data)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "gitea", p.Name)
|
||||
})
|
||||
|
||||
t.Run("zst", func(t *testing.T) {
|
||||
var zbuf bytes.Buffer
|
||||
zw, _ := zstd.NewWriter(&zbuf)
|
||||
zw.Write(buf.Bytes())
|
||||
zw.Close()
|
||||
|
||||
data := createArchive(map[string][]byte{"control.tar.zst": zbuf.Bytes()})
|
||||
|
||||
p, err := ParsePackage(data)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "gitea", p.Name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseControlFile(t *testing.T) {
|
||||
buildContent := func(name, version, architecture string) *bytes.Buffer {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("Package: " + name + "\nVersion: " + version + "\nArchitecture: " + architecture + "\nMaintainer: " + packageAuthor + " <kn4ck3r@gitea.io>\nHomepage: " + projectURL + "\nDepends: a,\n b\nDescription: Description\n with multiple\n lines.")
|
||||
return &buf
|
||||
}
|
||||
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
for _, name := range []string{"", "-cd"} {
|
||||
p, err := ParseControlFile(buildContent(name, packageVersion, packageArchitecture))
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidVersion", func(t *testing.T) {
|
||||
for _, version := range []string{"", "1-", ":1.0", "1_0"} {
|
||||
p, err := ParseControlFile(buildContent(packageName, version, packageArchitecture))
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidArchitecture", func(t *testing.T) {
|
||||
p, err := ParseControlFile(buildContent(packageName, packageVersion, ""))
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidArchitecture)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
content := buildContent(packageName, packageVersion, packageArchitecture)
|
||||
full := content.String()
|
||||
|
||||
p, err := ParseControlFile(content)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, p)
|
||||
|
||||
assert.Equal(t, packageName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.Equal(t, packageArchitecture, p.Architecture)
|
||||
assert.Equal(t, description, p.Metadata.Description)
|
||||
assert.Equal(t, projectURL, p.Metadata.ProjectURL)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Maintainer)
|
||||
assert.Equal(t, []string{"a", "b"}, p.Metadata.Dependencies)
|
||||
assert.Equal(t, full, p.Control)
|
||||
})
|
||||
}
|
||||
@@ -25,8 +25,15 @@ type HashedBuffer struct {
|
||||
combinedWriter io.Writer
|
||||
}
|
||||
|
||||
// NewHashedBuffer creates a hashed buffer with a specific maximum memory size
|
||||
func NewHashedBuffer(maxMemorySize int) (*HashedBuffer, error) {
|
||||
const DefaultMemorySize = 32 * 1024 * 1024
|
||||
|
||||
// NewHashedBuffer creates a hashed buffer with the default memory size
|
||||
func NewHashedBuffer() (*HashedBuffer, error) {
|
||||
return NewHashedBufferWithSize(DefaultMemorySize)
|
||||
}
|
||||
|
||||
// NewHashedBuffer creates a hashed buffer with a specific memory size
|
||||
func NewHashedBufferWithSize(maxMemorySize int) (*HashedBuffer, error) {
|
||||
b, err := filebuffer.New(maxMemorySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,9 +50,14 @@ func NewHashedBuffer(maxMemorySize int) (*HashedBuffer, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateHashedBufferFromReader creates a hashed buffer and copies the provided reader data into it.
|
||||
func CreateHashedBufferFromReader(r io.Reader, maxMemorySize int) (*HashedBuffer, error) {
|
||||
b, err := NewHashedBuffer(maxMemorySize)
|
||||
// CreateHashedBufferFromReader creates a hashed buffer with the default memory size and copies the provided reader data into it.
|
||||
func CreateHashedBufferFromReader(r io.Reader) (*HashedBuffer, error) {
|
||||
return CreateHashedBufferFromReaderWithSize(r, DefaultMemorySize)
|
||||
}
|
||||
|
||||
// CreateHashedBufferFromReaderWithSize creates a hashed buffer and copies the provided reader data into it.
|
||||
func CreateHashedBufferFromReaderWithSize(r io.Reader, maxMemorySize int) (*HashedBuffer, error) {
|
||||
b, err := NewHashedBufferWithSize(maxMemorySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestHashedBuffer(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
buf, err := CreateHashedBufferFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
|
||||
buf, err := CreateHashedBufferFromReaderWithSize(strings.NewReader(c.Data), c.MaxMemorySize)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, len(c.Data), buf.Size())
|
||||
|
||||
@@ -63,7 +63,7 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := packages.CreateHashedBufferFromReader(f, 32*1024*1024)
|
||||
buf, err := packages.CreateHashedBufferFromReader(f)
|
||||
|
||||
f.Close()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -101,6 +102,9 @@ func TestChannelQueue_Batch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChannelQueue_Pause(t *testing.T) {
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("Skipping because test is flaky on CI")
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
var queue Queue
|
||||
var err error
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -15,6 +16,10 @@ import (
|
||||
)
|
||||
|
||||
func TestPersistableChannelUniqueQueue(t *testing.T) {
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("Skipping because test is flaky on CI")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
_ = log.NewLogger(1000, "console", "console", `{"level":"warn","stacktracelevel":"NONE","stderr":true}`)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := repo_model.IsRepositoryExist(ctx, u, repo.Name)
|
||||
has, err := repo_model.IsRepositoryModelExist(ctx, u, repo.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IsRepositoryExist: %w", err)
|
||||
} else if has {
|
||||
|
||||
@@ -33,7 +33,7 @@ var Markdown = struct {
|
||||
}{
|
||||
EnableHardLineBreakInComments: true,
|
||||
EnableHardLineBreakInDocuments: false,
|
||||
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd", ","),
|
||||
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
|
||||
EnableMath: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ var (
|
||||
LimitSizeConan int64
|
||||
LimitSizeConda int64
|
||||
LimitSizeContainer int64
|
||||
LimitSizeDebian int64
|
||||
LimitSizeGeneric int64
|
||||
LimitSizeHelm int64
|
||||
LimitSizeMaven int64
|
||||
@@ -73,6 +74,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
|
||||
Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
|
||||
Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA")
|
||||
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
|
||||
Packages.LimitSizeDebian = mustBytes(sec, "LIMIT_SIZE_DEBIAN")
|
||||
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
|
||||
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
|
||||
Packages.LimitSizeMaven = mustBytes(sec, "LIMIT_SIZE_MAVEN")
|
||||
|
||||
@@ -168,7 +168,7 @@ var (
|
||||
Editor: struct {
|
||||
LineWrapExtensions []string
|
||||
}{
|
||||
LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,", ","),
|
||||
LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,.livemd,", ","),
|
||||
},
|
||||
|
||||
// Repository upload settings
|
||||
|
||||
@@ -250,6 +250,9 @@ func loadCommonSettingsFrom(cfg ConfigProvider) {
|
||||
loadLogFrom(cfg)
|
||||
loadServerFrom(cfg)
|
||||
loadSSHFrom(cfg)
|
||||
|
||||
mustCurrentRunUserMatch(cfg) // it depends on the SSH config, only non-builtin SSH server requires this check
|
||||
|
||||
loadOAuth2From(cfg)
|
||||
loadSecurityFrom(cfg)
|
||||
loadAttachmentFrom(cfg)
|
||||
@@ -282,14 +285,6 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
RunMode = rootSec.Key("RUN_MODE").MustString("prod")
|
||||
}
|
||||
IsProd = strings.EqualFold(RunMode, "prod")
|
||||
// Does not check run user when the install lock is off.
|
||||
installLock := rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
|
||||
if installLock {
|
||||
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
|
||||
if !match {
|
||||
log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
// check if we run as root
|
||||
if os.Getuid() == 0 {
|
||||
@@ -301,6 +296,17 @@ func loadRunModeFrom(rootCfg ConfigProvider) {
|
||||
}
|
||||
}
|
||||
|
||||
func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
|
||||
// Does not check run user when the "InstallLock" is off.
|
||||
installLock := rootCfg.Section("security").Key("INSTALL_LOCK").MustBool(false)
|
||||
if installLock {
|
||||
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
|
||||
if !match {
|
||||
log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSettings initializes the settings for normal start up
|
||||
func LoadSettings() {
|
||||
loadDBSetting(CfgProvider)
|
||||
|
||||
+2
-37
@@ -9,45 +9,10 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// Time settings
|
||||
TimeFormat string
|
||||
// UILocation is the location on the UI, so that we can display the time on UI.
|
||||
DefaultUILocation = time.Local
|
||||
)
|
||||
// DefaultUILocation is the location on the UI, so that we can display the time on UI.
|
||||
var DefaultUILocation = time.Local
|
||||
|
||||
func loadTimeFrom(rootCfg ConfigProvider) {
|
||||
timeFormatKey := rootCfg.Section("time").Key("FORMAT").MustString("")
|
||||
if timeFormatKey != "" {
|
||||
TimeFormat = map[string]string{
|
||||
"ANSIC": time.ANSIC,
|
||||
"UnixDate": time.UnixDate,
|
||||
"RubyDate": time.RubyDate,
|
||||
"RFC822": time.RFC822,
|
||||
"RFC822Z": time.RFC822Z,
|
||||
"RFC850": time.RFC850,
|
||||
"RFC1123": time.RFC1123,
|
||||
"RFC1123Z": time.RFC1123Z,
|
||||
"RFC3339": time.RFC3339,
|
||||
"RFC3339Nano": time.RFC3339Nano,
|
||||
"Kitchen": time.Kitchen,
|
||||
"Stamp": time.Stamp,
|
||||
"StampMilli": time.StampMilli,
|
||||
"StampMicro": time.StampMicro,
|
||||
"StampNano": time.StampNano,
|
||||
}[timeFormatKey]
|
||||
// When the TimeFormatKey does not exist in the previous map e.g.'2006-01-02 15:04:05'
|
||||
if len(TimeFormat) == 0 {
|
||||
TimeFormat = timeFormatKey
|
||||
TestTimeFormat, _ := time.Parse(TimeFormat, TimeFormat)
|
||||
if TestTimeFormat.Format(time.RFC3339) != "2006-01-02T15:04:05Z" {
|
||||
log.Warn("Provided TimeFormat: %s does not create a fully specified date and time.", TimeFormat)
|
||||
log.Warn("In order to display dates and times correctly please check your time format has 2006, 01, 02, 15, 04 and 05")
|
||||
}
|
||||
log.Trace("Custom TimeFormat: %s", TimeFormat)
|
||||
}
|
||||
}
|
||||
|
||||
zone := rootCfg.Section("time").Key("DEFAULT_UI_LOCATION").String()
|
||||
if zone != "" {
|
||||
var err error
|
||||
|
||||
@@ -72,6 +72,28 @@ type ServerVersion struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// GitignoreTemplateInfo name and text of a gitignore template
|
||||
type GitignoreTemplateInfo struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// LicensesListEntry is used for the API
|
||||
type LicensesTemplateListEntry struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// LicensesInfo contains information about a License
|
||||
type LicenseTemplateInfo struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Implementation string `json:"implementation"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// APIError is an api error with a message
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
|
||||
@@ -80,6 +80,7 @@ type Repository struct {
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated_at"`
|
||||
ArchivedAt time.Time `json:"archived_at"`
|
||||
Permissions *Permission `json:"permissions,omitempty"`
|
||||
HasIssues bool `json:"has_issues"`
|
||||
InternalTracker *InternalTracker `json:"internal_tracker,omitempty"`
|
||||
|
||||
+12
-556
@@ -5,54 +5,31 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"math"
|
||||
"mime"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
gitea_html "code.gitea.io/gitea/modules/html"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"code.gitea.io/gitea/modules/templates/eval"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
"github.com/editorconfig/editorconfig-core-go/v2"
|
||||
)
|
||||
|
||||
// Used from static.go && dynamic.go
|
||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
|
||||
|
||||
// NewFuncMap returns functions for injecting to templates
|
||||
func NewFuncMap() []template.FuncMap {
|
||||
return []template.FuncMap{map[string]interface{}{
|
||||
func NewFuncMap() template.FuncMap {
|
||||
return map[string]interface{}{
|
||||
"DumpVar": dumpVar,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// html/template related functions
|
||||
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
|
||||
@@ -63,6 +40,7 @@ func NewFuncMap() []template.FuncMap {
|
||||
"JSEscape": template.JSEscapeString,
|
||||
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
|
||||
"URLJoin": util.URLJoin,
|
||||
"DotEscape": DotEscape,
|
||||
|
||||
"PathEscape": url.PathEscape,
|
||||
"PathEscapeSegments": util.PathEscapeSegments,
|
||||
@@ -70,30 +48,7 @@ func NewFuncMap() []template.FuncMap {
|
||||
// utils
|
||||
"StringUtils": NewStringUtils,
|
||||
"SliceUtils": NewSliceUtils,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// string / json
|
||||
// TODO: move string helper functions to StringUtils
|
||||
"Join": strings.Join,
|
||||
"DotEscape": DotEscape,
|
||||
"EllipsisString": base.EllipsisString,
|
||||
"DumpVar": dumpVar,
|
||||
|
||||
"Json": func(in interface{}) string {
|
||||
out, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
},
|
||||
"JsonPrettyPrint": func(in string) string {
|
||||
var out bytes.Buffer
|
||||
err := json.Indent(&out, []byte(in), "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return out.String()
|
||||
},
|
||||
"JsonUtils": NewJsonUtils,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// svg / avatar / icon
|
||||
@@ -107,31 +62,7 @@ func NewFuncMap() []template.FuncMap {
|
||||
"MigrationIcon": MigrationIcon,
|
||||
"ActionIcon": ActionIcon,
|
||||
|
||||
"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
|
||||
// if needed
|
||||
if len(normSort) == 0 || len(urlSort) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(urlSort) == 0 && isDefault {
|
||||
// if sort is sorted as default add arrow tho this table header
|
||||
if isDefault {
|
||||
return svg.RenderHTML("octicon-triangle-down", 16)
|
||||
}
|
||||
} else {
|
||||
// if sort arg is in url test if it correlates with column header sort arguments
|
||||
// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
|
||||
if urlSort == normSort {
|
||||
// the table is sorted with this header normal
|
||||
return svg.RenderHTML("octicon-triangle-up", 16)
|
||||
} else if urlSort == revSort {
|
||||
// the table is sorted with this header reverse
|
||||
return svg.RenderHTML("octicon-triangle-down", 16)
|
||||
}
|
||||
}
|
||||
// the table is NOT sorted with this header
|
||||
return ""
|
||||
},
|
||||
"SortArrow": SortArrow,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
@@ -141,9 +72,6 @@ func NewFuncMap() []template.FuncMap {
|
||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||
"DateTime": timeutil.DateTime,
|
||||
"Sec2Time": util.SecToTime,
|
||||
"DateFmtLong": func(t time.Time) string {
|
||||
return t.Format(time.RFC3339)
|
||||
},
|
||||
"LoadTimes": func(startTime time.Time) string {
|
||||
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
||||
},
|
||||
@@ -245,32 +173,9 @@ func NewFuncMap() []template.FuncMap {
|
||||
"ReactionToEmoji": ReactionToEmoji,
|
||||
"RenderNote": RenderNote,
|
||||
|
||||
"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
|
||||
output, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: setting.AppSubURL,
|
||||
}, input)
|
||||
if err != nil {
|
||||
log.Error("RenderString: %v", err)
|
||||
}
|
||||
return template.HTML(output)
|
||||
},
|
||||
"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
|
||||
return template.HTML(RenderLabel(ctx, label))
|
||||
},
|
||||
"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
|
||||
htmlCode := `<span class="labels-list">`
|
||||
for _, label := range labels {
|
||||
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
||||
if label == nil {
|
||||
continue
|
||||
}
|
||||
htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
|
||||
repoLink, label.ID, RenderLabel(ctx, label))
|
||||
}
|
||||
htmlCode += "</span>"
|
||||
return template.HTML(htmlCode)
|
||||
},
|
||||
"RenderMarkdownToHtml": RenderMarkdownToHtml,
|
||||
"RenderLabel": RenderLabel,
|
||||
"RenderLabels": RenderLabels,
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// misc
|
||||
@@ -281,122 +186,9 @@ func NewFuncMap() []template.FuncMap {
|
||||
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
|
||||
"MirrorRemoteAddress": mirrorRemoteAddress,
|
||||
|
||||
"ParseDeadline": func(deadline string) []string {
|
||||
return strings.Split(deadline, "|")
|
||||
},
|
||||
"FilenameIsImage": func(filename string) bool {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
||||
return strings.HasPrefix(mimeType, "image/")
|
||||
},
|
||||
"TabSizeClass": func(ec interface{}, filename string) string {
|
||||
var (
|
||||
value *editorconfig.Editorconfig
|
||||
ok bool
|
||||
)
|
||||
if ec != nil {
|
||||
if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
|
||||
return "tab-size-8"
|
||||
}
|
||||
def, err := value.GetDefinitionForFilename(filename)
|
||||
if err != nil {
|
||||
log.Error("tab size class: getting definition for filename: %v", err)
|
||||
return "tab-size-8"
|
||||
}
|
||||
if def.TabWidth > 0 {
|
||||
return fmt.Sprintf("tab-size-%d", def.TabWidth)
|
||||
}
|
||||
}
|
||||
return "tab-size-8"
|
||||
},
|
||||
"SubJumpablePath": func(str string) []string {
|
||||
var path []string
|
||||
index := strings.LastIndex(str, "/")
|
||||
if index != -1 && index != len(str) {
|
||||
path = append(path, str[0:index+1], str[index+1:])
|
||||
} else {
|
||||
path = append(path, str)
|
||||
}
|
||||
return path
|
||||
},
|
||||
"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
|
||||
var curBranch string
|
||||
if repo.ID != baseRepo.ID {
|
||||
curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
|
||||
}
|
||||
curBranch += util.PathEscapeSegments(branchName)
|
||||
|
||||
return fmt.Sprintf("%s/compare/%s...%s",
|
||||
baseRepo.Link(),
|
||||
util.PathEscapeSegments(baseRepo.DefaultBranch),
|
||||
curBranch,
|
||||
)
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// AvatarHTML creates the HTML for an avatar
|
||||
func AvatarHTML(src string, size int, class, name string) template.HTML {
|
||||
sizeStr := fmt.Sprintf(`%d`, size)
|
||||
|
||||
if name == "" {
|
||||
name = "avatar"
|
||||
"FilenameIsImage": FilenameIsImage,
|
||||
"TabSizeClass": TabSizeClass,
|
||||
}
|
||||
|
||||
return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
||||
}
|
||||
|
||||
// Avatar renders user avatars. args: user, size (int), class (string)
|
||||
func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
|
||||
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
||||
|
||||
switch t := item.(type) {
|
||||
case *user_model.User:
|
||||
src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, t.DisplayName())
|
||||
}
|
||||
case *repo_model.Collaborator:
|
||||
src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, t.DisplayName())
|
||||
}
|
||||
case *organization.Organization:
|
||||
src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, t.AsUser().DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
|
||||
func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
|
||||
action.LoadActUser(ctx)
|
||||
return Avatar(ctx, action.ActUser, others...)
|
||||
}
|
||||
|
||||
// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
|
||||
func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
|
||||
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
||||
|
||||
src := repo.RelAvatarLink()
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, repo.FullName())
|
||||
}
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
|
||||
func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
|
||||
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
||||
src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
|
||||
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, name)
|
||||
}
|
||||
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// Safe render raw as HTML
|
||||
@@ -414,342 +206,6 @@ func DotEscape(raw string) string {
|
||||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
|
||||
}
|
||||
|
||||
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
||||
func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||
return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
|
||||
}
|
||||
|
||||
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
|
||||
// default url, handling for special links.
|
||||
func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
||||
cleanMsg := template.HTMLEscapeString(msg)
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
DefaultLink: urlDefault,
|
||||
Metas: metas,
|
||||
}, cleanMsg)
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
}
|
||||
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
|
||||
if len(msgLines) == 0 {
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(msgLines[0])
|
||||
}
|
||||
|
||||
// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
|
||||
// the provided default url, handling for special links without email to links.
|
||||
func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
||||
msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
|
||||
lineEnd := strings.IndexByte(msgLine, '\n')
|
||||
if lineEnd > 0 {
|
||||
msgLine = msgLine[:lineEnd]
|
||||
}
|
||||
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
|
||||
if len(msgLine) == 0 {
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
DefaultLink: urlDefault,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessageSubject: %v", err)
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(renderedMessage)
|
||||
}
|
||||
|
||||
// RenderCommitBody extracts the body of a commit message without its title.
|
||||
func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||
msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
|
||||
lineEnd := strings.IndexByte(msgLine, '\n')
|
||||
if lineEnd > 0 {
|
||||
msgLine = msgLine[lineEnd+1:]
|
||||
} else {
|
||||
return template.HTML("")
|
||||
}
|
||||
msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
|
||||
if len(msgLine) == 0 {
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
}
|
||||
return template.HTML(renderedMessage)
|
||||
}
|
||||
|
||||
// Match text that is between back ticks.
|
||||
var codeMatcher = regexp.MustCompile("`([^`]+)`")
|
||||
|
||||
// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
|
||||
// Intended for issue and PR titles, these containers should have styles for "<code>" elements
|
||||
func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
|
||||
htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
|
||||
return template.HTML(htmlWithCodeTags)
|
||||
}
|
||||
|
||||
// RenderIssueTitle renders issue/pull title with defined post processors
|
||||
func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
|
||||
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderIssueTitle: %v", err)
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(renderedText)
|
||||
}
|
||||
|
||||
// RenderLabel renders a label
|
||||
func RenderLabel(ctx context.Context, label *issues_model.Label) string {
|
||||
labelScope := label.ExclusiveScope()
|
||||
|
||||
textColor := "#111"
|
||||
if label.UseLightTextColor() {
|
||||
textColor = "#eee"
|
||||
}
|
||||
|
||||
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
||||
|
||||
if labelScope == "" {
|
||||
// Regular label
|
||||
return fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
|
||||
textColor, label.Color, description, RenderEmoji(ctx, label.Name))
|
||||
}
|
||||
|
||||
// Scoped label
|
||||
scopeText := RenderEmoji(ctx, labelScope)
|
||||
itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
|
||||
|
||||
itemColor := label.Color
|
||||
scopeColor := label.Color
|
||||
if r, g, b, err := label.ColorRGB(); err == nil {
|
||||
// Make scope and item background colors slightly darker and lighter respectively.
|
||||
// More contrast needed with higher luminance, empirically tweaked.
|
||||
luminance := (0.299*r + 0.587*g + 0.114*b) / 255
|
||||
contrast := 0.01 + luminance*0.03
|
||||
// Ensure we add the same amount of contrast also near 0 and 1.
|
||||
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
|
||||
lighten := contrast + math.Max(contrast-luminance, 0.0)
|
||||
// Compute factor to keep RGB values proportional.
|
||||
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
|
||||
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
|
||||
|
||||
scopeBytes := []byte{
|
||||
uint8(math.Min(math.Round(r*darkenFactor), 255)),
|
||||
uint8(math.Min(math.Round(g*darkenFactor), 255)),
|
||||
uint8(math.Min(math.Round(b*darkenFactor), 255)),
|
||||
}
|
||||
itemBytes := []byte{
|
||||
uint8(math.Min(math.Round(r*lightenFactor), 255)),
|
||||
uint8(math.Min(math.Round(g*lightenFactor), 255)),
|
||||
uint8(math.Min(math.Round(b*lightenFactor), 255)),
|
||||
}
|
||||
|
||||
itemColor = "#" + hex.EncodeToString(itemBytes)
|
||||
scopeColor = "#" + hex.EncodeToString(scopeBytes)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
|
||||
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
|
||||
"</span>",
|
||||
description,
|
||||
textColor, scopeColor, scopeText,
|
||||
textColor, itemColor, itemText)
|
||||
}
|
||||
|
||||
// RenderEmoji renders html text with emoji post processors
|
||||
func RenderEmoji(ctx context.Context, text string) template.HTML {
|
||||
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
|
||||
template.HTMLEscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderEmoji: %v", err)
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(renderedText)
|
||||
}
|
||||
|
||||
// ReactionToEmoji renders emoji for use in reactions
|
||||
func ReactionToEmoji(reaction string) template.HTML {
|
||||
val := emoji.FromCode(reaction)
|
||||
if val != nil {
|
||||
return template.HTML(val.Emoji)
|
||||
}
|
||||
val = emoji.FromAlias(reaction)
|
||||
if val != nil {
|
||||
return template.HTML(val.Emoji)
|
||||
}
|
||||
return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
|
||||
}
|
||||
|
||||
// RenderNote renders the contents of a git-notes file as a commit message.
|
||||
func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||
cleanMsg := template.HTMLEscapeString(msg)
|
||||
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
Metas: metas,
|
||||
}, cleanMsg)
|
||||
if err != nil {
|
||||
log.Error("RenderNote: %v", err)
|
||||
return ""
|
||||
}
|
||||
return template.HTML(fullMessage)
|
||||
}
|
||||
|
||||
// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
|
||||
func IsMultilineCommitMessage(msg string) bool {
|
||||
return strings.Count(strings.TrimSpace(msg), "\n") >= 1
|
||||
}
|
||||
|
||||
// Actioner describes an action
|
||||
type Actioner interface {
|
||||
GetOpType() activities_model.ActionType
|
||||
GetActUserName() string
|
||||
GetRepoUserName() string
|
||||
GetRepoName() string
|
||||
GetRepoPath() string
|
||||
GetRepoLink() string
|
||||
GetBranch() string
|
||||
GetContent() string
|
||||
GetCreate() time.Time
|
||||
GetIssueInfos() []string
|
||||
}
|
||||
|
||||
// ActionIcon accepts an action operation type and returns an icon class name.
|
||||
func ActionIcon(opType activities_model.ActionType) string {
|
||||
switch opType {
|
||||
case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
|
||||
return "repo"
|
||||
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
|
||||
return "git-commit"
|
||||
case activities_model.ActionCreateIssue:
|
||||
return "issue-opened"
|
||||
case activities_model.ActionCreatePullRequest:
|
||||
return "git-pull-request"
|
||||
case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
|
||||
return "comment-discussion"
|
||||
case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
|
||||
return "git-merge"
|
||||
case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
|
||||
return "issue-closed"
|
||||
case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
|
||||
return "issue-reopened"
|
||||
case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
|
||||
return "mirror"
|
||||
case activities_model.ActionApprovePullRequest:
|
||||
return "check"
|
||||
case activities_model.ActionRejectPullRequest:
|
||||
return "diff"
|
||||
case activities_model.ActionPublishRelease:
|
||||
return "tag"
|
||||
case activities_model.ActionPullReviewDismissed:
|
||||
return "x"
|
||||
default:
|
||||
return "question"
|
||||
}
|
||||
}
|
||||
|
||||
// ActionContent2Commits converts action content to push commits
|
||||
func ActionContent2Commits(act Actioner) *repository.PushCommits {
|
||||
push := repository.NewPushCommits()
|
||||
|
||||
if act == nil || act.GetContent() == "" {
|
||||
return push
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
|
||||
log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
|
||||
}
|
||||
|
||||
if push.Len == 0 {
|
||||
push.Len = len(push.Commits)
|
||||
}
|
||||
|
||||
return push
|
||||
}
|
||||
|
||||
// DiffLineTypeToStr returns diff line type name
|
||||
func DiffLineTypeToStr(diffType int) string {
|
||||
switch diffType {
|
||||
case 2:
|
||||
return "add"
|
||||
case 3:
|
||||
return "del"
|
||||
case 4:
|
||||
return "tag"
|
||||
}
|
||||
return "same"
|
||||
}
|
||||
|
||||
// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
|
||||
func MigrationIcon(hostname string) string {
|
||||
switch hostname {
|
||||
case "github.com":
|
||||
return "octicon-mark-github"
|
||||
default:
|
||||
return "gitea-git"
|
||||
}
|
||||
}
|
||||
|
||||
type remoteAddress struct {
|
||||
Address string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
|
||||
a := remoteAddress{}
|
||||
|
||||
remoteURL := m.OriginalURL
|
||||
if ignoreOriginalURL || remoteURL == "" {
|
||||
var err error
|
||||
remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
|
||||
if err != nil {
|
||||
log.Error("GetRemoteURL %v", err)
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
u, err := giturl.Parse(remoteURL)
|
||||
if err != nil {
|
||||
log.Error("giturl.Parse %v", err)
|
||||
return a
|
||||
}
|
||||
|
||||
if u.Scheme != "ssh" && u.Scheme != "file" {
|
||||
if u.User != nil {
|
||||
a.Username = u.User.Username()
|
||||
a.Password, _ = u.User.Password()
|
||||
}
|
||||
u.User = nil
|
||||
}
|
||||
a.Address = u.String()
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Eval the expression and return the result, see the comment of eval.Expr for details.
|
||||
// To use this helper function in templates, pass each token as a separate parameter.
|
||||
//
|
||||
|
||||
@@ -6,7 +6,6 @@ package templates
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -15,24 +14,29 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
texttemplate "text/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/assetfs"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates/scopedtmpl"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var rendererKey interface{} = "templatesHtmlRenderer"
|
||||
|
||||
type TemplateExecutor scopedtmpl.TemplateExecutor
|
||||
|
||||
type HTMLRender struct {
|
||||
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
|
||||
}
|
||||
|
||||
var (
|
||||
htmlRender *HTMLRender
|
||||
htmlRenderOnce sync.Once
|
||||
)
|
||||
|
||||
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
|
||||
|
||||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
|
||||
@@ -55,14 +59,14 @@ func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) {
|
||||
return nil, ErrTemplateNotInitialized
|
||||
}
|
||||
|
||||
return tmpls.Executor(name, NewFuncMap()[0])
|
||||
return tmpls.Executor(name, NewFuncMap())
|
||||
}
|
||||
|
||||
func (h *HTMLRender) CompileTemplates() error {
|
||||
assets := AssetFS()
|
||||
extSuffix := ".tmpl"
|
||||
tmpls := scopedtmpl.NewScopedTemplate()
|
||||
tmpls.Funcs(NewFuncMap()[0])
|
||||
tmpls.Funcs(NewFuncMap())
|
||||
files, err := ListWebTemplateAssetNames(assets)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -86,20 +90,21 @@ func (h *HTMLRender) CompileTemplates() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
|
||||
func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
|
||||
if renderer, ok := ctx.Value(rendererKey).(*HTMLRender); ok {
|
||||
return ctx, renderer
|
||||
}
|
||||
// HTMLRenderer init once and returns the globally shared html renderer
|
||||
func HTMLRenderer() *HTMLRender {
|
||||
htmlRenderOnce.Do(initHTMLRenderer)
|
||||
return htmlRender
|
||||
}
|
||||
|
||||
func initHTMLRenderer() {
|
||||
rendererType := "static"
|
||||
if !setting.IsProd {
|
||||
rendererType = "auto-reloading"
|
||||
}
|
||||
log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
|
||||
log.Debug("Creating %s HTML Renderer", rendererType)
|
||||
|
||||
renderer := &HTMLRender{}
|
||||
if err := renderer.CompileTemplates(); err != nil {
|
||||
htmlRender = &HTMLRender{}
|
||||
if err := htmlRender.CompileTemplates(); err != nil {
|
||||
p := &templateErrorPrettier{assets: AssetFS()}
|
||||
wrapFatal(p.handleFuncNotDefinedError(err))
|
||||
wrapFatal(p.handleUnexpectedOperandError(err))
|
||||
@@ -107,14 +112,14 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
|
||||
wrapFatal(p.handleGenericTemplateError(err))
|
||||
log.Fatal("HTMLRenderer CompileTemplates error: %v", err)
|
||||
}
|
||||
|
||||
if !setting.IsProd {
|
||||
go AssetFS().WatchLocalChanges(ctx, func() {
|
||||
if err := renderer.CompileTemplates(); err != nil {
|
||||
go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() {
|
||||
if err := htmlRender.CompileTemplates(); err != nil {
|
||||
log.Error("Template error: %v\n%s", err, log.Stack(2))
|
||||
}
|
||||
})
|
||||
}
|
||||
return context.WithValue(ctx, rendererKey, renderer), renderer
|
||||
}
|
||||
|
||||
func wrapFatal(msg string) {
|
||||
|
||||
@@ -6,6 +6,7 @@ package templates
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||
|
||||
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||
func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
||||
return texttmpl.FuncMap{
|
||||
@@ -55,9 +58,7 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
bodyTemplates := template.New("")
|
||||
|
||||
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
||||
for _, funcs := range NewFuncMap() {
|
||||
bodyTemplates.Funcs(funcs)
|
||||
}
|
||||
bodyTemplates.Funcs(NewFuncMap())
|
||||
|
||||
assetFS := AssetFS()
|
||||
refreshTemplates := func() {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
gitea_html "code.gitea.io/gitea/modules/html"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// AvatarHTML creates the HTML for an avatar
|
||||
func AvatarHTML(src string, size int, class, name string) template.HTML {
|
||||
sizeStr := fmt.Sprintf(`%d`, size)
|
||||
|
||||
if name == "" {
|
||||
name = "avatar"
|
||||
}
|
||||
|
||||
return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
||||
}
|
||||
|
||||
// Avatar renders user avatars. args: user, size (int), class (string)
|
||||
func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
|
||||
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
||||
|
||||
switch t := item.(type) {
|
||||
case *user_model.User:
|
||||
src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, t.DisplayName())
|
||||
}
|
||||
case *repo_model.Collaborator:
|
||||
src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, t.DisplayName())
|
||||
}
|
||||
case *organization.Organization:
|
||||
src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, t.AsUser().DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
|
||||
func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
|
||||
action.LoadActUser(ctx)
|
||||
return Avatar(ctx, action.ActUser, others...)
|
||||
}
|
||||
|
||||
// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
|
||||
func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
|
||||
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
||||
|
||||
src := repo.RelAvatarLink()
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, repo.FullName())
|
||||
}
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
|
||||
func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
|
||||
size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
|
||||
src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
|
||||
|
||||
if src != "" {
|
||||
return AvatarHTML(src, size, class, name)
|
||||
}
|
||||
|
||||
return template.HTML("")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
type JsonUtils struct{} //nolint:revive
|
||||
|
||||
var jsonUtils = JsonUtils{}
|
||||
|
||||
func NewJsonUtils() *JsonUtils { //nolint:revive
|
||||
return &jsonUtils
|
||||
}
|
||||
|
||||
func (su *JsonUtils) EncodeToString(v any) string {
|
||||
out, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (su *JsonUtils) PrettyIndent(s string) string {
|
||||
var out bytes.Buffer
|
||||
err := json.Indent(&out, []byte(s), "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
|
||||
"github.com/editorconfig/editorconfig-core-go/v2"
|
||||
)
|
||||
|
||||
func SortArrow(normSort, revSort, urlSort string, isDefault bool) template.HTML {
|
||||
// if needed
|
||||
if len(normSort) == 0 || len(urlSort) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(urlSort) == 0 && isDefault {
|
||||
// if sort is sorted as default add arrow tho this table header
|
||||
if isDefault {
|
||||
return svg.RenderHTML("octicon-triangle-down", 16)
|
||||
}
|
||||
} else {
|
||||
// if sort arg is in url test if it correlates with column header sort arguments
|
||||
// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
|
||||
if urlSort == normSort {
|
||||
// the table is sorted with this header normal
|
||||
return svg.RenderHTML("octicon-triangle-up", 16)
|
||||
} else if urlSort == revSort {
|
||||
// the table is sorted with this header reverse
|
||||
return svg.RenderHTML("octicon-triangle-down", 16)
|
||||
}
|
||||
}
|
||||
// the table is NOT sorted with this header
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
|
||||
func IsMultilineCommitMessage(msg string) bool {
|
||||
return strings.Count(strings.TrimSpace(msg), "\n") >= 1
|
||||
}
|
||||
|
||||
// Actioner describes an action
|
||||
type Actioner interface {
|
||||
GetOpType() activities_model.ActionType
|
||||
GetActUserName() string
|
||||
GetRepoUserName() string
|
||||
GetRepoName() string
|
||||
GetRepoPath() string
|
||||
GetRepoLink() string
|
||||
GetBranch() string
|
||||
GetContent() string
|
||||
GetCreate() time.Time
|
||||
GetIssueInfos() []string
|
||||
}
|
||||
|
||||
// ActionIcon accepts an action operation type and returns an icon class name.
|
||||
func ActionIcon(opType activities_model.ActionType) string {
|
||||
switch opType {
|
||||
case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
|
||||
return "repo"
|
||||
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
|
||||
return "git-commit"
|
||||
case activities_model.ActionCreateIssue:
|
||||
return "issue-opened"
|
||||
case activities_model.ActionCreatePullRequest:
|
||||
return "git-pull-request"
|
||||
case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
|
||||
return "comment-discussion"
|
||||
case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
|
||||
return "git-merge"
|
||||
case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
|
||||
return "issue-closed"
|
||||
case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
|
||||
return "issue-reopened"
|
||||
case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
|
||||
return "mirror"
|
||||
case activities_model.ActionApprovePullRequest:
|
||||
return "check"
|
||||
case activities_model.ActionRejectPullRequest:
|
||||
return "diff"
|
||||
case activities_model.ActionPublishRelease:
|
||||
return "tag"
|
||||
case activities_model.ActionPullReviewDismissed:
|
||||
return "x"
|
||||
default:
|
||||
return "question"
|
||||
}
|
||||
}
|
||||
|
||||
// ActionContent2Commits converts action content to push commits
|
||||
func ActionContent2Commits(act Actioner) *repository.PushCommits {
|
||||
push := repository.NewPushCommits()
|
||||
|
||||
if act == nil || act.GetContent() == "" {
|
||||
return push
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
|
||||
log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
|
||||
}
|
||||
|
||||
if push.Len == 0 {
|
||||
push.Len = len(push.Commits)
|
||||
}
|
||||
|
||||
return push
|
||||
}
|
||||
|
||||
// DiffLineTypeToStr returns diff line type name
|
||||
func DiffLineTypeToStr(diffType int) string {
|
||||
switch diffType {
|
||||
case 2:
|
||||
return "add"
|
||||
case 3:
|
||||
return "del"
|
||||
case 4:
|
||||
return "tag"
|
||||
}
|
||||
return "same"
|
||||
}
|
||||
|
||||
// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
|
||||
func MigrationIcon(hostname string) string {
|
||||
switch hostname {
|
||||
case "github.com":
|
||||
return "octicon-mark-github"
|
||||
default:
|
||||
return "gitea-git"
|
||||
}
|
||||
}
|
||||
|
||||
type remoteAddress struct {
|
||||
Address string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
|
||||
a := remoteAddress{}
|
||||
|
||||
remoteURL := m.OriginalURL
|
||||
if ignoreOriginalURL || remoteURL == "" {
|
||||
var err error
|
||||
remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
|
||||
if err != nil {
|
||||
log.Error("GetRemoteURL %v", err)
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
u, err := giturl.Parse(remoteURL)
|
||||
if err != nil {
|
||||
log.Error("giturl.Parse %v", err)
|
||||
return a
|
||||
}
|
||||
|
||||
if u.Scheme != "ssh" && u.Scheme != "file" {
|
||||
if u.User != nil {
|
||||
a.Username = u.User.Username()
|
||||
a.Password, _ = u.User.Password()
|
||||
}
|
||||
u.User = nil
|
||||
}
|
||||
a.Address = u.String()
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func FilenameIsImage(filename string) bool {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
||||
return strings.HasPrefix(mimeType, "image/")
|
||||
}
|
||||
|
||||
func TabSizeClass(ec interface{}, filename string) string {
|
||||
var (
|
||||
value *editorconfig.Editorconfig
|
||||
ok bool
|
||||
)
|
||||
if ec != nil {
|
||||
if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
|
||||
return "tab-size-8"
|
||||
}
|
||||
def, err := value.GetDefinitionForFilename(filename)
|
||||
if err != nil {
|
||||
log.Error("tab size class: getting definition for filename: %v", err)
|
||||
return "tab-size-8"
|
||||
}
|
||||
if def.TabWidth > 0 {
|
||||
return fmt.Sprintf("tab-size-%d", def.TabWidth)
|
||||
}
|
||||
}
|
||||
return "tab-size-8"
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
||||
func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||
return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
|
||||
}
|
||||
|
||||
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
|
||||
// default url, handling for special links.
|
||||
func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
||||
cleanMsg := template.HTMLEscapeString(msg)
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
DefaultLink: urlDefault,
|
||||
Metas: metas,
|
||||
}, cleanMsg)
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
}
|
||||
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
|
||||
if len(msgLines) == 0 {
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(msgLines[0])
|
||||
}
|
||||
|
||||
// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
|
||||
// the provided default url, handling for special links without email to links.
|
||||
func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
|
||||
msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
|
||||
lineEnd := strings.IndexByte(msgLine, '\n')
|
||||
if lineEnd > 0 {
|
||||
msgLine = msgLine[:lineEnd]
|
||||
}
|
||||
msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
|
||||
if len(msgLine) == 0 {
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// we can safely assume that it will not return any error, since there
|
||||
// shouldn't be any special HTML.
|
||||
renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
DefaultLink: urlDefault,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessageSubject: %v", err)
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(renderedMessage)
|
||||
}
|
||||
|
||||
// RenderCommitBody extracts the body of a commit message without its title.
|
||||
func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||
msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
|
||||
lineEnd := strings.IndexByte(msgLine, '\n')
|
||||
if lineEnd > 0 {
|
||||
msgLine = msgLine[lineEnd+1:]
|
||||
} else {
|
||||
return template.HTML("")
|
||||
}
|
||||
msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
|
||||
if len(msgLine) == 0 {
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(msgLine))
|
||||
if err != nil {
|
||||
log.Error("RenderCommitMessage: %v", err)
|
||||
return ""
|
||||
}
|
||||
return template.HTML(renderedMessage)
|
||||
}
|
||||
|
||||
// Match text that is between back ticks.
|
||||
var codeMatcher = regexp.MustCompile("`([^`]+)`")
|
||||
|
||||
// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
|
||||
// Intended for issue and PR titles, these containers should have styles for "<code>" elements
|
||||
func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
|
||||
htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
|
||||
return template.HTML(htmlWithCodeTags)
|
||||
}
|
||||
|
||||
// RenderIssueTitle renders issue/pull title with defined post processors
|
||||
func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
|
||||
renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
Metas: metas,
|
||||
}, template.HTMLEscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderIssueTitle: %v", err)
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(renderedText)
|
||||
}
|
||||
|
||||
// RenderLabel renders a label
|
||||
func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
|
||||
labelScope := label.ExclusiveScope()
|
||||
|
||||
textColor := "#111"
|
||||
if label.UseLightTextColor() {
|
||||
textColor = "#eee"
|
||||
}
|
||||
|
||||
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
|
||||
|
||||
if labelScope == "" {
|
||||
// Regular label
|
||||
s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
|
||||
textColor, label.Color, description, RenderEmoji(ctx, label.Name))
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
// Scoped label
|
||||
scopeText := RenderEmoji(ctx, labelScope)
|
||||
itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
|
||||
|
||||
itemColor := label.Color
|
||||
scopeColor := label.Color
|
||||
if r, g, b, err := label.ColorRGB(); err == nil {
|
||||
// Make scope and item background colors slightly darker and lighter respectively.
|
||||
// More contrast needed with higher luminance, empirically tweaked.
|
||||
luminance := (0.299*r + 0.587*g + 0.114*b) / 255
|
||||
contrast := 0.01 + luminance*0.03
|
||||
// Ensure we add the same amount of contrast also near 0 and 1.
|
||||
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
|
||||
lighten := contrast + math.Max(contrast-luminance, 0.0)
|
||||
// Compute factor to keep RGB values proportional.
|
||||
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
|
||||
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
|
||||
|
||||
scopeBytes := []byte{
|
||||
uint8(math.Min(math.Round(r*darkenFactor), 255)),
|
||||
uint8(math.Min(math.Round(g*darkenFactor), 255)),
|
||||
uint8(math.Min(math.Round(b*darkenFactor), 255)),
|
||||
}
|
||||
itemBytes := []byte{
|
||||
uint8(math.Min(math.Round(r*lightenFactor), 255)),
|
||||
uint8(math.Min(math.Round(g*lightenFactor), 255)),
|
||||
uint8(math.Min(math.Round(b*lightenFactor), 255)),
|
||||
}
|
||||
|
||||
itemColor = "#" + hex.EncodeToString(itemBytes)
|
||||
scopeColor = "#" + hex.EncodeToString(scopeBytes)
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
|
||||
"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
|
||||
"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
|
||||
"</span>",
|
||||
description,
|
||||
textColor, scopeColor, scopeText,
|
||||
textColor, itemColor, itemText)
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
// RenderEmoji renders html text with emoji post processors
|
||||
func RenderEmoji(ctx context.Context, text string) template.HTML {
|
||||
renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
|
||||
template.HTMLEscapeString(text))
|
||||
if err != nil {
|
||||
log.Error("RenderEmoji: %v", err)
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(renderedText)
|
||||
}
|
||||
|
||||
// ReactionToEmoji renders emoji for use in reactions
|
||||
func ReactionToEmoji(reaction string) template.HTML {
|
||||
val := emoji.FromCode(reaction)
|
||||
if val != nil {
|
||||
return template.HTML(val.Emoji)
|
||||
}
|
||||
val = emoji.FromAlias(reaction)
|
||||
if val != nil {
|
||||
return template.HTML(val.Emoji)
|
||||
}
|
||||
return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
|
||||
}
|
||||
|
||||
// RenderNote renders the contents of a git-notes file as a commit message.
|
||||
func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||
cleanMsg := template.HTMLEscapeString(msg)
|
||||
fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: urlPrefix,
|
||||
Metas: metas,
|
||||
}, cleanMsg)
|
||||
if err != nil {
|
||||
log.Error("RenderNote: %v", err)
|
||||
return ""
|
||||
}
|
||||
return template.HTML(fullMessage)
|
||||
}
|
||||
|
||||
func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
|
||||
output, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: setting.AppSubURL,
|
||||
}, input)
|
||||
if err != nil {
|
||||
log.Error("RenderString: %v", err)
|
||||
}
|
||||
return template.HTML(output)
|
||||
}
|
||||
|
||||
func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
|
||||
htmlCode := `<span class="labels-list">`
|
||||
for _, label := range labels {
|
||||
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
|
||||
if label == nil {
|
||||
continue
|
||||
}
|
||||
htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
|
||||
repoLink, label.ID, RenderLabel(ctx, label))
|
||||
}
|
||||
htmlCode += "</span>"
|
||||
return template.HTML(htmlCode)
|
||||
}
|
||||
@@ -3,12 +3,18 @@
|
||||
|
||||
package templates
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
)
|
||||
|
||||
type StringUtils struct{}
|
||||
|
||||
var stringUtils = StringUtils{}
|
||||
|
||||
func NewStringUtils() *StringUtils {
|
||||
return &StringUtils{}
|
||||
return &stringUtils
|
||||
}
|
||||
|
||||
func (su *StringUtils) HasPrefix(s, prefix string) bool {
|
||||
@@ -18,3 +24,15 @@ func (su *StringUtils) HasPrefix(s, prefix string) bool {
|
||||
func (su *StringUtils) Contains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
func (su *StringUtils) Split(s, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
func (su *StringUtils) Join(a []string, sep string) string {
|
||||
return strings.Join(a, sep)
|
||||
}
|
||||
|
||||
func (su *StringUtils) EllipsisString(s string, max int) string {
|
||||
return base.EllipsisString(s, max)
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ func DateTime(format string, datetime any) template.HTML {
|
||||
var datetimeEscaped, textEscaped string
|
||||
switch v := datetime.(type) {
|
||||
case nil:
|
||||
return "N/A"
|
||||
return "-"
|
||||
case string:
|
||||
datetimeEscaped = html.EscapeString(v)
|
||||
textEscaped = datetimeEscaped
|
||||
case time.Time:
|
||||
if v.IsZero() || v.Unix() == 0 {
|
||||
return "N/A"
|
||||
return "-"
|
||||
}
|
||||
datetimeEscaped = html.EscapeString(v.Format(time.RFC3339))
|
||||
if format == "full" {
|
||||
|
||||
@@ -23,10 +23,10 @@ func TestDateTime(t *testing.T) {
|
||||
refTime, _ := time.Parse(time.RFC3339, refTimeStr)
|
||||
refTimeStamp := TimeStamp(refTime.Unix())
|
||||
|
||||
assert.EqualValues(t, "N/A", DateTime("short", nil))
|
||||
assert.EqualValues(t, "N/A", DateTime("short", 0))
|
||||
assert.EqualValues(t, "N/A", DateTime("short", time.Time{}))
|
||||
assert.EqualValues(t, "N/A", DateTime("short", TimeStamp(0)))
|
||||
assert.EqualValues(t, "-", DateTime("short", nil))
|
||||
assert.EqualValues(t, "-", DateTime("short", 0))
|
||||
assert.EqualValues(t, "-", DateTime("short", time.Time{}))
|
||||
assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
|
||||
|
||||
actual := DateTime("short", "invalid")
|
||||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="invalid">invalid</relative-time>`, actual)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package timeutil
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
var langTimeFormats = map[string]string{
|
||||
"zh-CN": "2006年01月02日 15时04分05秒",
|
||||
"en-US": time.RFC1123,
|
||||
"lv-LV": "02.01.2006. 15:04:05",
|
||||
}
|
||||
|
||||
// GetLangTimeFormat represents the default time format for the language
|
||||
func GetLangTimeFormat(lang string) string {
|
||||
return langTimeFormats[lang]
|
||||
}
|
||||
|
||||
// GetTimeFormat represents the
|
||||
func GetTimeFormat(lang string) string {
|
||||
if setting.TimeFormat == "" {
|
||||
format := GetLangTimeFormat(lang)
|
||||
if format == "" {
|
||||
format = time.RFC1123
|
||||
}
|
||||
return format
|
||||
}
|
||||
return setting.TimeFormat
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation/i18n"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
@@ -241,5 +242,12 @@ func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
|
||||
|
||||
func (l *locale) PrettyNumber(v any) string {
|
||||
// TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format
|
||||
if s, ok := v.(string); ok {
|
||||
if num, err := util.ToInt64(s); err == nil {
|
||||
v = num
|
||||
} else if num, err := util.ToFloat64(s); err == nil {
|
||||
v = num
|
||||
}
|
||||
}
|
||||
return l.msgPrinter.Sprintf("%v", number.Decimal(v))
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ func TestPrettyNumber(t *testing.T) {
|
||||
|
||||
l := NewLocale("id-ID")
|
||||
assert.EqualValues(t, "1.000.000", l.PrettyNumber(1000000))
|
||||
assert.EqualValues(t, "1.000.000,1", l.PrettyNumber(1000000.1))
|
||||
assert.EqualValues(t, "1.000.000", l.PrettyNumber("1000000"))
|
||||
assert.EqualValues(t, "1.000.000", l.PrettyNumber("1000000.0"))
|
||||
assert.EqualValues(t, "1.000.000,1", l.PrettyNumber("1000000.1"))
|
||||
|
||||
l = NewLocale("nosuch")
|
||||
assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000))
|
||||
assert.EqualValues(t, "1,000,000.1", l.PrettyNumber(1000000.1))
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
)
|
||||
|
||||
const maxInt = int(^uint(0) >> 1) // taken from bytes.Buffer
|
||||
|
||||
var (
|
||||
// ErrInvalidMemorySize occurs if the memory size is not in a valid range
|
||||
ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32")
|
||||
@@ -37,7 +36,7 @@ type FileBackedBuffer struct {
|
||||
|
||||
// New creates a file backed buffer with a specific maximum memory size
|
||||
func New(maxMemorySize int) (*FileBackedBuffer, error) {
|
||||
if maxMemorySize < 0 || maxMemorySize > maxInt {
|
||||
if maxMemorySize < 0 || maxMemorySize > math.MaxInt32 {
|
||||
return nil, ErrInvalidMemorySize
|
||||
}
|
||||
|
||||
|
||||
+10
-28
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
@@ -131,16 +130,22 @@ func hasResponseBeenWritten(argsIn []reflect.Value) bool {
|
||||
// toHandlerProvider converts a handler to a handler provider
|
||||
// A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware
|
||||
func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
|
||||
if hp, ok := handler.(func(next http.Handler) http.Handler); ok {
|
||||
return hp
|
||||
}
|
||||
|
||||
funcInfo := routing.GetFuncInfo(handler)
|
||||
fn := reflect.ValueOf(handler)
|
||||
if fn.Type().Kind() != reflect.Func {
|
||||
panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type()))
|
||||
}
|
||||
|
||||
if hp, ok := handler.(func(next http.Handler) http.Handler); ok {
|
||||
return func(next http.Handler) http.Handler {
|
||||
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
h.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
provider := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
|
||||
// wrap the response writer to check whether the response has been written
|
||||
@@ -175,26 +180,3 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
|
||||
provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported
|
||||
return provider
|
||||
}
|
||||
|
||||
// MiddlewareWithPrefix wraps a handler function at a prefix, and make it as a middleware
|
||||
// TODO: this design is incorrect, the asset handler should not be a middleware
|
||||
func MiddlewareWithPrefix(pathPrefix string, middleware func(handler http.Handler) http.Handler, handlerFunc http.HandlerFunc) func(next http.Handler) http.Handler {
|
||||
funcInfo := routing.GetFuncInfo(handlerFunc)
|
||||
handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
handlerFunc(resp, req)
|
||||
})
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
if !strings.HasPrefix(req.URL.Path, pathPrefix) {
|
||||
next.ServeHTTP(resp, req)
|
||||
return
|
||||
}
|
||||
if middleware != nil {
|
||||
middleware(handler).ServeHTTP(resp, req)
|
||||
} else {
|
||||
handler.ServeHTTP(resp, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+4
-16
@@ -44,23 +44,13 @@ type Route struct {
|
||||
// NewRoute creates a new route
|
||||
func NewRoute() *Route {
|
||||
r := chi.NewRouter()
|
||||
return &Route{
|
||||
R: r,
|
||||
curGroupPrefix: "",
|
||||
curMiddlewares: []interface{}{},
|
||||
}
|
||||
return &Route{R: r}
|
||||
}
|
||||
|
||||
// Use supports two middlewares
|
||||
func (r *Route) Use(middlewares ...interface{}) {
|
||||
if r.curGroupPrefix != "" {
|
||||
// FIXME: this behavior is incorrect, should use "With" instead
|
||||
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
|
||||
} else {
|
||||
// FIXME: another misuse, the "Use" with empty middlewares is called after "Mount"
|
||||
for _, m := range middlewares {
|
||||
r.R.Use(toHandlerProvider(m))
|
||||
}
|
||||
for _, m := range middlewares {
|
||||
r.R.Use(toHandlerProvider(m))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,9 +106,7 @@ func (r *Route) Methods(method, pattern string, h []any) {
|
||||
|
||||
// Mount attaches another Route along ./pattern/*
|
||||
func (r *Route) Mount(pattern string, subR *Route) {
|
||||
middlewares := make([]interface{}, len(r.curMiddlewares))
|
||||
copy(middlewares, r.curMiddlewares)
|
||||
subR.Use(middlewares...)
|
||||
subR.Use(r.curMiddlewares...)
|
||||
r.R.Mount(r.getPattern(pattern), subR.R)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user