Merge remote-tracking branch 'upstream/main' into limit-repo-size

This commit is contained in:
DmitryFrolovTri
2023-05-03 14:58:32 +00:00
495 changed files with 6917 additions and 12240 deletions
+2 -1
View File
@@ -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
}
+21 -1
View File
@@ -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
+40
View File
@@ -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"))
}
+1 -1
View File
@@ -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{
+33
View File
@@ -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
}
}
}
+2 -2
View File
@@ -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
+13 -5
View File
@@ -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)
}
+218
View File
@@ -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
}
+171
View File
@@ -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)
})
}
+17 -5
View File
@@ -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
}
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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
View File
@@ -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}`)
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
}
+2
View File
@@ -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")
+1 -1
View File
@@ -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
+14 -8
View File
@@ -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
View File
@@ -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
+22
View File
@@ -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"`
+1
View File
@@ -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
View File
@@ -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.
//
+21 -16
View File
@@ -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) {
+4 -3
View File
@@ -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() {
+84
View File
@@ -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("")
}
+35
View File
@@ -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()
}
+209
View File
@@ -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"
}
+254
View File
@@ -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)
}
+20 -2
View File
@@ -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)
}
+2 -2
View File
@@ -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" {
+4 -4
View File
@@ -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)
-33
View File
@@ -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
}
+8
View File
@@ -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))
}
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}