mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-01 20:59:45 +09:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a0e125018 | |||
| 1c19e2ba43 | |||
| b0d6cc1d22 | |||
| 8936303510 | |||
| 3d085dbaf1 | |||
| 9b2a3c267b | |||
| 590d2984d9 | |||
| 61e0343580 | |||
| 0b43bde974 | |||
| cc95f141f8 | |||
| a25fbd43c4 | |||
| f365a6b9c8 | |||
| e0095af8c3 |
+17
-1
@@ -21,8 +21,19 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func acmeExternalAccountBinding() (*acme.EAB, error) {
|
||||||
|
if setting.AcmeEABKID == "" && setting.AcmeEABHMAC == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if setting.AcmeEABKID == "" || setting.AcmeEABHMAC == "" {
|
||||||
|
return nil, errors.New("both ACME_EAB_KID and ACME_EAB_HMAC must be set")
|
||||||
|
}
|
||||||
|
return &acme.EAB{KeyID: setting.AcmeEABKID, MACKey: setting.AcmeEABHMAC}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getCARoot(path string) (*x509.CertPool, error) {
|
func getCARoot(path string) (*x509.CertPool, error) {
|
||||||
r, err := os.ReadFile(path)
|
r, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,6 +77,10 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
|
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
externalAccount, err := acmeExternalAccountBinding()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
|
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
|
||||||
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
|
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
|
||||||
// And one more thing, no idea why we should set the global default variables here
|
// And one more thing, no idea why we should set the global default variables here
|
||||||
@@ -84,6 +99,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
Email: setting.AcmeEmail,
|
Email: setting.AcmeEmail,
|
||||||
Agreed: setting.AcmeTOS,
|
Agreed: setting.AcmeTOS,
|
||||||
Profile: setting.AcmeProfile,
|
Profile: setting.AcmeProfile,
|
||||||
|
ExternalAccount: externalAccount,
|
||||||
DisableHTTPChallenge: !enableHTTPChallenge,
|
DisableHTTPChallenge: !enableHTTPChallenge,
|
||||||
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
|
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
|
||||||
ListenHost: setting.HTTPAddr,
|
ListenHost: setting.HTTPAddr,
|
||||||
@@ -100,7 +116,7 @@ func runACME(listenAddr string, m http.Handler) error {
|
|||||||
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
||||||
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
||||||
ctx := graceful.GetManager().ShutdownContext()
|
ctx := graceful.GetManager().ShutdownContext()
|
||||||
err := magic.ManageSync(ctx, []string{setting.AppDomain})
|
err = magic.ManageSync(ctx, []string{setting.AppDomain})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
|
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
|
||||||
if cacheErr != nil || cert.Expired() {
|
if cacheErr != nil || cert.Expired() {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAcmeExternalAccountBinding(t *testing.T) {
|
||||||
|
t.Cleanup(test.MockVariableValue(&setting.AcmeEABKID, ""))
|
||||||
|
t.Cleanup(test.MockVariableValue(&setting.AcmeEABHMAC, ""))
|
||||||
|
|
||||||
|
binding, err := acmeExternalAccountBinding()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, binding)
|
||||||
|
|
||||||
|
setting.AcmeEABKID = "kid"
|
||||||
|
_, err = acmeExternalAccountBinding()
|
||||||
|
assert.ErrorContains(t, err, "both ACME_EAB_KID and ACME_EAB_HMAC must be set")
|
||||||
|
|
||||||
|
setting.AcmeEABHMAC = "hmac"
|
||||||
|
binding, err = acmeExternalAccountBinding()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, &acme.EAB{KeyID: "kid", MACKey: "hmac"}, binding)
|
||||||
|
}
|
||||||
+21
-18
@@ -155,7 +155,7 @@
|
|||||||
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
|
||||||
;BUILTIN_SSH_SERVER_USER =
|
;BUILTIN_SSH_SERVER_USER =
|
||||||
;;
|
;;
|
||||||
;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
|
;; Domain name to be exposed in clone URL, defaults to the domain part of ROOT_URL
|
||||||
;SSH_DOMAIN =
|
;SSH_DOMAIN =
|
||||||
;;
|
;;
|
||||||
;; Port number to be exposed in clone URL.
|
;; Port number to be exposed in clone URL.
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
;; For the built-in SSH server, choose the keypair to offer as the host key
|
;; For the built-in SSH server, choose the keypair to offer as the host key
|
||||||
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
|
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
|
||||||
;; relative paths are made absolute relative to the APP_DATA_PATH
|
;; relative paths are made absolute relative to the APP_DATA_PATH
|
||||||
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
|
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gitea.ed25519, ssh/gitea.ecdsa, ssh/gogs.rsa
|
||||||
;;
|
;;
|
||||||
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
|
||||||
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
||||||
;;
|
;;
|
||||||
;; Indicate whether to check minimum key size with corresponding type
|
;; Indicate whether to check minimum key size with corresponding type
|
||||||
;MINIMUM_KEY_SIZE_CHECK = false
|
;MINIMUM_KEY_SIZE_CHECK = true
|
||||||
;;
|
;;
|
||||||
;; TLS Settings: Either ACME or manual
|
;; TLS Settings: Either ACME or manual
|
||||||
;; (Other common TLS configuration are found before)
|
;; (Other common TLS configuration are found before)
|
||||||
@@ -264,6 +264,11 @@
|
|||||||
;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates)
|
;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates)
|
||||||
;ACME_PROFILE =
|
;ACME_PROFILE =
|
||||||
;;
|
;;
|
||||||
|
;; External account binding credentials; set both to enable EAB
|
||||||
|
;; ACME_EAB_HMAC should be a base64url-encoded MAC key
|
||||||
|
;ACME_EAB_KID =
|
||||||
|
;ACME_EAB_HMAC =
|
||||||
|
;;
|
||||||
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
|
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
|
||||||
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
|
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
|
||||||
;ACME_DIRECTORY = https
|
;ACME_DIRECTORY = https
|
||||||
@@ -836,7 +841,7 @@ LEVEL = Info
|
|||||||
;EMAIL_DOMAIN_BLOCKLIST =
|
;EMAIL_DOMAIN_BLOCKLIST =
|
||||||
;;
|
;;
|
||||||
;; Disallow registration, only allow admins to create accounts.
|
;; Disallow registration, only allow admins to create accounts.
|
||||||
;DISABLE_REGISTRATION = false
|
;DISABLE_REGISTRATION = true
|
||||||
;;
|
;;
|
||||||
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
;; Allow registration only using gitea itself, it works only when DISABLE_REGISTRATION is false
|
||||||
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
;ALLOW_ONLY_INTERNAL_REGISTRATION = false
|
||||||
@@ -968,12 +973,11 @@ LEVEL = Info
|
|||||||
;; Value for the domain part of the user's email address in the git log if user
|
;; Value for the domain part of the user's email address in the git log if user
|
||||||
;; has set KeepEmailPrivate to true. The user's email will be replaced with a
|
;; has set KeepEmailPrivate to true. The user's email will be replaced with a
|
||||||
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
|
;; concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. Default
|
||||||
;; value is "noreply." + DOMAIN, where DOMAIN resolves to the value from server.DOMAIN
|
;; value is "noreply." + the domain part of ROOT_URL
|
||||||
;; Note: do not use the <DOMAIN> notation below
|
;NO_REPLY_ADDRESS =
|
||||||
;NO_REPLY_ADDRESS = ; noreply.<DOMAIN>
|
|
||||||
;;
|
;;
|
||||||
;; Show Registration button
|
;; Show Registration button, defaults to true only if both DISABLE_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are false
|
||||||
;SHOW_REGISTRATION_BUTTON = true
|
;SHOW_REGISTRATION_BUTTON = false
|
||||||
;;
|
;;
|
||||||
;; Show milestones dashboard page - a view of all the user's milestones
|
;; Show milestones dashboard page - a view of all the user's milestones
|
||||||
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
;SHOW_MILESTONES_DASHBOARD_PAGE = true
|
||||||
@@ -1670,13 +1674,13 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy
|
;; General queue type, currently support: level, channel, redis, dummy
|
||||||
;; default to persistable-channel
|
;; default to level
|
||||||
;TYPE = persistable-channel
|
;TYPE = level
|
||||||
;;
|
;;
|
||||||
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
;; data-dir for storing level queues, individual queues will default to `queues/common` meaning the queue is shared.
|
||||||
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
;; Relative paths will be made absolute against "APP_DATA_PATH"
|
||||||
;DATADIR = queues/
|
;DATADIR = queues/common
|
||||||
;;
|
;;
|
||||||
;; Default queue length before a channel queue will block
|
;; Default queue length before a channel queue will block
|
||||||
;LENGTH = 100000
|
;LENGTH = 100000
|
||||||
@@ -1684,7 +1688,7 @@ LEVEL = Info
|
|||||||
;; Batch size to send for batched queues
|
;; Batch size to send for batched queues
|
||||||
;BATCH_LENGTH = 20
|
;BATCH_LENGTH = 20
|
||||||
;;
|
;;
|
||||||
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
|
;; When `TYPE` is `level`, this provides a directory for the underlying leveldb
|
||||||
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
|
||||||
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
||||||
;CONN_STR =
|
;CONN_STR =
|
||||||
@@ -1752,7 +1756,6 @@ LEVEL = Info
|
|||||||
;ENABLE_OPENID_SIGNIN = false
|
;ENABLE_OPENID_SIGNIN = false
|
||||||
;;
|
;;
|
||||||
;; Whether to allow registering via OpenID
|
;; Whether to allow registering via OpenID
|
||||||
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
|
|
||||||
;;ENABLE_OPENID_SIGNUP = false
|
;;ENABLE_OPENID_SIGNUP = false
|
||||||
;;
|
;;
|
||||||
;; Allowed URI patterns (POSIX regexp).
|
;; Allowed URI patterns (POSIX regexp).
|
||||||
@@ -2015,8 +2018,8 @@ LEVEL = Info
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
|
||||||
;; Default is "memory". "db" will reuse the configuration in [database]
|
;; Default is "file". "db" will reuse the configuration in [database]
|
||||||
;PROVIDER = memory
|
;PROVIDER = file
|
||||||
;;
|
;;
|
||||||
;; Provider config options
|
;; Provider config options
|
||||||
;; memory: doesn't have any config yet
|
;; memory: doesn't have any config yet
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.24
|
github.com/mattn/go-isatty v0.0.24
|
||||||
github.com/mattn/go-sqlite3 v1.14.52
|
github.com/mattn/go-sqlite3 v1.14.52
|
||||||
github.com/meilisearch/meilisearch-go v0.36.3
|
github.com/meilisearch/meilisearch-go v0.36.3
|
||||||
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/microsoft/go-mssqldb v1.11.2
|
github.com/microsoft/go-mssqldb v1.11.2
|
||||||
@@ -199,7 +200,6 @@ require (
|
|||||||
github.com/markbates/going v1.0.3 // indirect
|
github.com/markbates/going v1.0.3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
|
||||||
github.com/miekg/dns v1.1.72 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
|
|||||||
@@ -295,9 +295,8 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er
|
|||||||
return &run, nil
|
return &run, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) ([]*ActionRun, error) {
|
func GetRunsByRepoAndID(ctx context.Context, repoID int64, runIDs []int64) (runs []*ActionRun, err error) {
|
||||||
var runs []*ActionRun
|
err = db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).OrderBy("id").Find(&runs)
|
||||||
err := db.GetEngine(ctx).In("id", runIDs).Where("repo_id=?", repoID).Find(&runs)
|
|
||||||
return runs, err
|
return runs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ type SearchUserOptions struct {
|
|||||||
Keyword string
|
Keyword string
|
||||||
Types []UserType
|
Types []UserType
|
||||||
UID int64
|
UID int64
|
||||||
LoginName string // this option should be used only for admin user
|
LoginName string // this option should be used only for admin user
|
||||||
SourceID int64 // this option should be used only for admin user
|
SourceID optional.Option[int64] // this option should be used only for admin user, Some(0) means local users
|
||||||
OrderBy db.SearchOrderBy
|
OrderBy db.SearchOrderBy
|
||||||
Visible []structs.VisibleType
|
Visible []structs.VisibleType
|
||||||
Actor *User // The user doing the search
|
Actor *User // The user doing the search
|
||||||
@@ -106,8 +106,8 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session
|
|||||||
cond = cond.And(builder.Eq{"id": opts.UID})
|
cond = cond.And(builder.Eq{"id": opts.UID})
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.SourceID > 0 {
|
if opts.SourceID.Has() {
|
||||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
|
cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
|
||||||
}
|
}
|
||||||
if opts.LoginName != "" {
|
if opts.LoginName != "" {
|
||||||
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/translation"
|
"gitea.dev/modules/translation"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type htmlChunkReader struct {
|
type htmlChunkReader struct {
|
||||||
@@ -30,6 +32,10 @@ type escapeStreamer struct {
|
|||||||
ambiguousTables []*AmbiguousTable
|
ambiguousTables []*AmbiguousTable
|
||||||
allowed map[rune]bool
|
allowed map[rune]bool
|
||||||
|
|
||||||
|
tagPartial []byte // partial tag content, used to detect if we are in some tags
|
||||||
|
|
||||||
|
inTagMath bool // MathML operators like U+2212 are intended and wrapping them breaks the math layout
|
||||||
|
|
||||||
out io.Writer
|
out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +68,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
|||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if partInTag[i] {
|
if partInTag[i] {
|
||||||
lastIsTag = true
|
lastIsTag = true
|
||||||
|
es.trackHtmlTag(part)
|
||||||
if _, err := out.Write(part); err != nil {
|
if _, err := out.Write(part); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -75,7 +82,11 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = es.detectAndWriteRunes(part); err != nil {
|
if es.inTagMath {
|
||||||
|
if _, err := out.Write(part); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err = es.detectAndWriteRunes(part); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +94,34 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trackHtmlTag receives tag parts, a tag might be split into multiple parts
|
||||||
|
func (e *escapeStreamer) trackHtmlTag(part []byte) {
|
||||||
|
const maxHeadLen = 100 // only read the first N bytes of the tag for detection purpose
|
||||||
|
if part[0] == '<' {
|
||||||
|
// start a new tag
|
||||||
|
e.tagPartial = e.tagPartial[:0]
|
||||||
|
}
|
||||||
|
if len(e.tagPartial) >= maxHeadLen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.tagPartial = append(e.tagPartial, part[:min(len(part), maxHeadLen-len(e.tagPartial))]...)
|
||||||
|
|
||||||
|
isTag := func(prefix string) bool {
|
||||||
|
if len(e.tagPartial) < len(prefix)+1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !util.AsciiEqualFold(e.tagPartial[:len(prefix)], []byte(prefix)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.IndexByte(" \t\n\r\f>", e.tagPartial[len(prefix)]) != -1
|
||||||
|
}
|
||||||
|
if isTag("<math") {
|
||||||
|
e.inTagMath = true
|
||||||
|
} else if isTag("</math") {
|
||||||
|
e.inTagMath = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
|
func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) {
|
||||||
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
|
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
|
|||||||
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
|
result: `O<span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:𝐾 [U+1D43E],K [U+004B]"><span class="char">𝐾</span></span>`,
|
||||||
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ambiguous in math",
|
||||||
|
text: "<math><mo>−</mo><mi>b</mi></math> −",
|
||||||
|
result: `<math><mo>−</mo><mi>b</mi></math> <span class="ambiguous-code-point" data-tooltip-content="repo.ambiguous_character:− [U+2212],- [U+002D]"><span class="char">−</span></span>`,
|
||||||
|
status: EscapeStatus{Escaped: true, HasAmbiguous: true},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeControlReader(t *testing.T) {
|
func TestEscapeControlReader(t *testing.T) {
|
||||||
@@ -156,6 +162,24 @@ func TestEscapeControlReader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrackHtmlTag(t *testing.T) {
|
||||||
|
e := &escapeStreamer{}
|
||||||
|
for _, tt := range []struct {
|
||||||
|
parts []string
|
||||||
|
inMath bool
|
||||||
|
}{
|
||||||
|
{[]string{"<ma", `TH display="block">`}, true},
|
||||||
|
{[]string{"<mo>"}, true},
|
||||||
|
{[]string{"</MA", "th>"}, false},
|
||||||
|
{[]string{"<mathx>"}, false},
|
||||||
|
} {
|
||||||
|
for _, part := range tt.parts {
|
||||||
|
e.trackHtmlTag([]byte(part))
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.inMath, e.inTagMath, "%v", tt.parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
func TestSettingAmbiguousUnicodeDetection(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
||||||
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
_, out := EscapeControlHTML("a test", &translation.MockLocale{})
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
|||||||
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
|
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
|
||||||
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
|
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
|
||||||
return node.NextSibling
|
return node.NextSibling
|
||||||
} else if node.Data == "code" || node.Data == "pre" {
|
} else if node.Data == "code" || node.Data == "pre" || node.Data == "math" {
|
||||||
return node.NextSibling // ignore code and pre nodes
|
return node.NextSibling // ignore code, pre and math nodes
|
||||||
} else if node.Data == "img" {
|
} else if node.Data == "img" {
|
||||||
return visitNodeImg(ctx, node)
|
return visitNodeImg(ctx, node)
|
||||||
} else if node.Data == "video" {
|
} else if node.Data == "video" {
|
||||||
|
|||||||
@@ -543,6 +543,9 @@ func TestPostProcess(t *testing.T) {
|
|||||||
`Some text with <span class="emoji" data-alias="smile">😄</span> in the middle`)
|
`Some text with <span class="emoji" data-alias="smile">😄</span> in the middle`)
|
||||||
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
|
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
|
||||||
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
|
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
|
||||||
|
test(
|
||||||
|
"<math><mtext>:gitea: go-gitea/gitea#12345</mtext></math>",
|
||||||
|
"<math><mtext>:gitea: go-gitea/gitea#12345</mtext></math>")
|
||||||
|
|
||||||
// special tags, GitHub's behavior, and for unclosed tags, output as text content as much as possible
|
// special tags, GitHub's behavior, and for unclosed tags, output as text content as much as possible
|
||||||
test("<script>a", `<script>a`)
|
test("<script>a", `<script>a`)
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ var (
|
|||||||
AcmeEmail string
|
AcmeEmail string
|
||||||
AcmeURL string
|
AcmeURL string
|
||||||
AcmeProfile string
|
AcmeProfile string
|
||||||
|
AcmeEABKID string
|
||||||
|
AcmeEABHMAC string
|
||||||
AcmeCARoot string
|
AcmeCARoot string
|
||||||
SSLMinimumVersion string
|
SSLMinimumVersion string
|
||||||
SSLMaximumVersion string
|
SSLMaximumVersion string
|
||||||
@@ -144,6 +146,11 @@ func loadServerDomainAndURL(sec ConfigSection, protocol string) {
|
|||||||
AppDomain = appURL.Hostname()
|
AppDomain = appURL.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadAcmeEABFrom(sec ConfigSection) {
|
||||||
|
AcmeEABKID = sec.Key("ACME_EAB_KID").MustString("")
|
||||||
|
AcmeEABHMAC = sec.Key("ACME_EAB_HMAC").MustString("")
|
||||||
|
}
|
||||||
|
|
||||||
func loadServerFrom(rootCfg ConfigProvider) {
|
func loadServerFrom(rootCfg ConfigProvider) {
|
||||||
sec := rootCfg.Section("server")
|
sec := rootCfg.Section("server")
|
||||||
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
|
||||||
@@ -173,6 +180,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
if EnableAcme {
|
if EnableAcme {
|
||||||
AcmeURL = sec.Key("ACME_URL").MustString("")
|
AcmeURL = sec.Key("ACME_URL").MustString("")
|
||||||
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
|
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
|
||||||
|
loadAcmeEABFrom(sec)
|
||||||
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
||||||
|
|
||||||
if sec.HasKey("ACME_ACCEPTTOS") {
|
if sec.HasKey("ACME_ACCEPTTOS") {
|
||||||
@@ -208,6 +216,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
KeyFile = filepath.Join(CustomPath, KeyFile)
|
KeyFile = filepath.Join(CustomPath, KeyFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
|
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
|
||||||
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
|
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
|
||||||
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
|
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadAcmeEABFrom(t *testing.T) {
|
||||||
|
t.Cleanup(test.MockVariableValue(&AcmeEABKID, ""))
|
||||||
|
t.Cleanup(test.MockVariableValue(&AcmeEABHMAC, ""))
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[server]
|
||||||
|
ACME_EAB_KID = kid
|
||||||
|
ACME_EAB_HMAC = hmac
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
loadAcmeEABFrom(cfg.Section("server"))
|
||||||
|
|
||||||
|
assert.Equal(t, "kid", AcmeEABKID)
|
||||||
|
assert.Equal(t, "hmac", AcmeEABHMAC)
|
||||||
|
}
|
||||||
@@ -121,7 +121,7 @@ func asciiLower(b byte) byte {
|
|||||||
|
|
||||||
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
|
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
|
||||||
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
|
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
|
||||||
func AsciiEqualFold(s, t string) bool {
|
func AsciiEqualFold[T string | []byte](s, t T) bool {
|
||||||
if len(s) != len(t) {
|
if len(s) != len(t) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,13 +20,17 @@ type RouterPathGroup struct {
|
|||||||
r *Router
|
r *Router
|
||||||
pathParam string
|
pathParam string
|
||||||
matchers []*routerPathMatcher
|
matchers []*routerPathMatcher
|
||||||
|
unescape bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
chiCtx := chi.RouteContext(req.Context())
|
chiCtx := chi.RouteContext(req.Context())
|
||||||
path := chiCtx.URLParam(g.pathParam)
|
path := chiCtx.URLParam(g.pathParam)
|
||||||
|
if g.unescape {
|
||||||
|
path, _ = url.PathUnescape(path)
|
||||||
|
}
|
||||||
for _, m := range g.matchers {
|
for _, m := range g.matchers {
|
||||||
if m.matchPath(chiCtx, path) {
|
if m.matchPath(chiCtx, path, g.unescape) {
|
||||||
chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern)
|
chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern)
|
||||||
executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc)
|
executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc)
|
||||||
return
|
return
|
||||||
@@ -53,6 +58,10 @@ func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupP
|
|||||||
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *RouterPathGroup) UseUnescapedPath() {
|
||||||
|
g.unescape = true
|
||||||
|
}
|
||||||
|
|
||||||
type routerPathParam struct {
|
type routerPathParam struct {
|
||||||
name string
|
name string
|
||||||
pathSepEnd bool
|
pathSepEnd bool
|
||||||
@@ -68,7 +77,7 @@ type routerPathMatcher struct {
|
|||||||
handlerFunc http.HandlerFunc
|
handlerFunc http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
|
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string, unescaped bool) bool {
|
||||||
if !p.methods.Contains(chiCtx.RouteMethod) {
|
if !p.methods.Contains(chiCtx.RouteMethod) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -102,6 +111,9 @@ func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
|
|||||||
if p.params[i].pathSepEnd {
|
if p.params[i].pathSepEnd {
|
||||||
val = strings.TrimSuffix(val, "/")
|
val = strings.TrimSuffix(val, "/")
|
||||||
}
|
}
|
||||||
|
if unescaped {
|
||||||
|
val = url.PathEscape(val)
|
||||||
|
}
|
||||||
chiCtx.URLParams.Add(p.params[i].name, val)
|
chiCtx.URLParams.Add(p.params[i].name, val)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -97,12 +98,16 @@ func (r *testRecorder) test(t *testing.T, rt *Router, methodPath string, expecte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPathProcessor(t *testing.T) {
|
func TestPathProcessor(t *testing.T) {
|
||||||
|
unescape := false
|
||||||
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
||||||
chiCtx := chi.NewRouteContext()
|
chiCtx := chi.NewRouteContext()
|
||||||
chiCtx.RouteMethod = "GET"
|
chiCtx.RouteMethod = "GET"
|
||||||
p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
|
p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
|
||||||
shouldProcess := expectedPathParams != nil
|
shouldProcess := expectedPathParams != nil
|
||||||
assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
if unescape {
|
||||||
|
uri, _ = url.PathUnescape(uri)
|
||||||
|
}
|
||||||
|
assert.Equal(t, shouldProcess, p.matchPath(chiCtx, uri, unescape), "use pattern %s to process uri %s", pattern, uri)
|
||||||
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +124,9 @@ func TestPathProcessor(t *testing.T) {
|
|||||||
testProcess("/<p1:*>/part/<p2>", "/part/c", map[string]string{"p1": "", "p2": "c"})
|
testProcess("/<p1:*>/part/<p2>", "/part/c", map[string]string{"p1": "", "p2": "c"})
|
||||||
testProcess("/<p1:*>/part/<p2>", "/a/other-part/c", nil)
|
testProcess("/<p1:*>/part/<p2>", "/a/other-part/c", nil)
|
||||||
testProcess("/<p1:*>-part/<p2>", "/a-other-part/c", map[string]string{"p1": "a-other", "p2": "c"})
|
testProcess("/<p1:*>-part/<p2>", "/a-other-part/c", map[string]string{"p1": "a-other", "p2": "c"})
|
||||||
|
|
||||||
|
unescape = true
|
||||||
|
testProcess("/<p1:@/x>", "/%40%2fx", map[string]string{"p1": "@%2Fx"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouter(t *testing.T) {
|
func TestRouter(t *testing.T) {
|
||||||
|
|||||||
+17
-30
@@ -405,37 +405,24 @@ func CommonRoutes() *web.Router {
|
|||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
})
|
})
|
||||||
r.Group("/npm", func() {
|
r.Group("/npm", func() {
|
||||||
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
|
r.Get("/-/v1/search", npm.PackageSearch)
|
||||||
scopeRegexp := `^@` + npm_module.RegexpNamePart + `$`
|
r.PathGroup("/*", func(g *web.RouterPathGroup) {
|
||||||
idRegexp := `^(@` + npm_module.RegexpNamePart + `%2[fF])?` + npm_module.RegexpNamePart + `$`
|
// HINT: NPM-ROUTE-PATH-PATTERN: search this keyword to see more details
|
||||||
addPackageHandlers := func() {
|
packageId := `/<id:(@` + npm_module.RegexpNamePart + `/)?` + npm_module.RegexpNamePart + ">"
|
||||||
r.Get("", npm.PackageMetadata)
|
g.UseUnescapedPath()
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
g.MatchPath("DELETE", packageId+"/-/<version>/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
||||||
r.Get("/{version}", npm.PackageVersionMetadata)
|
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFile)
|
||||||
r.Group("/-/{version}/{filename}", func() {
|
g.MatchPath("GET", packageId+"/-/<filename>", npm.DownloadPackageFileByName)
|
||||||
r.Get("", npm.DownloadPackageFile)
|
g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
|
||||||
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
|
g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
|
||||||
})
|
g.MatchPath("GET", packageId+"/<version>", npm.PackageVersionMetadata)
|
||||||
r.Get("/-/{filename}", npm.DownloadPackageFileByName)
|
g.MatchPath("GET", packageId, npm.PackageMetadata)
|
||||||
r.Group("/-rev/{revision}", func() {
|
g.MatchPath("PUT", packageId, reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
||||||
r.Delete("", npm.DeletePackage)
|
|
||||||
r.Put("", npm.DeletePreview)
|
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
|
||||||
}
|
|
||||||
r.Group("/{scope:"+scopeRegexp+"}/{id:"+idRegexp+"}", addPackageHandlers)
|
|
||||||
r.Group("/{id:"+idRegexp+"}", addPackageHandlers)
|
|
||||||
|
|
||||||
addPackageDistTagsHandlers := func() {
|
packageDistTags := "/-/package" + packageId + "/dist-tags"
|
||||||
r.Get("", npm.ListPackageTags)
|
g.MatchPath("GET", packageDistTags, npm.ListPackageTags)
|
||||||
r.Group("/{tag}", func() {
|
g.MatchPath("PUT", packageDistTags+"/<tag>", reqPackageAccess(perm.AccessModeWrite), npm.AddPackageTag)
|
||||||
r.Put("", npm.AddPackageTag)
|
g.MatchPath("DELETE", packageDistTags+"/<tag>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageTag)
|
||||||
r.Delete("", npm.DeletePackageTag)
|
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
|
||||||
}
|
|
||||||
r.Group("/-/package/{scope:"+scopeRegexp+"}/{id:"+idRegexp+"}/dist-tags", addPackageDistTagsHandlers)
|
|
||||||
r.Group("/-/package/{id:"+idRegexp+"}/dist-tags", addPackageDistTagsHandlers)
|
|
||||||
r.Group("/-/v1/search", func() {
|
|
||||||
r.Get("", npm.PackageSearch)
|
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, reqPackageAccess(perm.AccessModeRead))
|
||||||
r.Group("/pub", func() {
|
r.Group("/pub", func() {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
std_ctx "context"
|
std_ctx "context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -44,21 +43,12 @@ func apiError(ctx *context.Context, status int, obj any) {
|
|||||||
|
|
||||||
// packageNameFromParams gets the package name from the url parameters
|
// packageNameFromParams gets the package name from the url parameters
|
||||||
func packageNameFromParams(ctx *context.Context) string {
|
func packageNameFromParams(ctx *context.Context) string {
|
||||||
// Real examples: these 2 both should work:
|
// HINT: NPM-ROUTE-PATH-PATTERN: real examples: these cases all should work:
|
||||||
// * "https://registry.npmjs.org/@angular/core"
|
// * "https://registry.npmjs.org/@angular/core"
|
||||||
// * "https://registry.npmjs.org/@angular%2Fcore"
|
// * "https://registry.npmjs.org/@angular%2Fcore"
|
||||||
|
// * "https://registry.npmjs.org/%40angular%2Fcore"
|
||||||
//
|
//
|
||||||
// HINT: NPM-ROUTE-PATH-PATTERN: The cases for the path parameters:
|
return ctx.PathParam("id") // id is the full package name, e.g.: "@angular/core" or "lodash"
|
||||||
// * ".../TheName/...": id="TheName"
|
|
||||||
// * ".../@TheScope/TheName/...": scope="@TheScope", id="TheName"
|
|
||||||
// * ".../@TheScope%2FTheName/...": id="@TheScope/TheName"
|
|
||||||
scope := ctx.PathParam("scope")
|
|
||||||
fullOrSub := ctx.PathParam("id") // may be a full name or a subpath of the full package name
|
|
||||||
if scope != "" {
|
|
||||||
// now id is the subpath of the full package name, e.g. "core" in "@angular/core"
|
|
||||||
return fmt.Sprintf("%s/%s", scope, fullOrSub)
|
|
||||||
}
|
|
||||||
return fullOrSub // id is the full package name, e.g.: "@angular/core" or "lodash"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
|
func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ func SearchUsers(ctx *context.APIContext) {
|
|||||||
// parameters:
|
// parameters:
|
||||||
// - name: source_id
|
// - name: source_id
|
||||||
// in: query
|
// in: query
|
||||||
// description: ID of the user's login source to search for
|
// description: ID of the user's login source to search for, 0 means the local users
|
||||||
// type: integer
|
// type: integer
|
||||||
// format: int64
|
// format: int64
|
||||||
// - name: login_name
|
// - name: login_name
|
||||||
@@ -483,7 +483,7 @@ func SearchUsers(ctx *context.APIContext) {
|
|||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||||
LoginName: ctx.FormTrim("login_name"),
|
LoginName: ctx.FormTrim("login_name"),
|
||||||
SourceID: ctx.FormInt64("source_id"),
|
SourceID: ctx.FormOptionalInt64("source_id"),
|
||||||
Keyword: ctx.FormTrim("q"),
|
Keyword: ctx.FormTrim("q"),
|
||||||
Visible: visible,
|
Visible: visible,
|
||||||
OrderBy: orderBy,
|
OrderBy: orderBy,
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// if it's not a pointer, just serve the data directly
|
// if it's not a pointer, just serve the data directly
|
||||||
if !pointer.IsValid() {
|
if !pointer.IsValid() {
|
||||||
_, _ = ctx.Resp.Write(lfsPointerBuf)
|
httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, int64(len(lfsPointerBuf)), bytes.NewReader(lfsPointerBuf), httplib.ServeHeaderOptions{Filename: blob.Name()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// If there isn't one, just serve the data directly
|
// If there isn't one, just serve the data directly
|
||||||
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
|
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
|
||||||
_, _ = ctx.Resp.Write(lfsPointerBuf)
|
httplib.ServeUserContentByReader(ctx.Req, ctx.Resp, int64(len(lfsPointerBuf)), bytes.NewReader(lfsPointerBuf), httplib.ServeHeaderOptions{Filename: blob.Name()})
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -198,7 +198,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer lfsDataFile.Close()
|
defer lfsDataFile.Close()
|
||||||
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
|
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: blob.Name()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
|
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
|
||||||
|
|||||||
@@ -386,8 +386,8 @@ func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_mo
|
|||||||
ctx.APIErrorNotFound("no such attachment in repo")
|
ctx.APIErrorNotFound("no such attachment in repo")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if attachment.IssueID == 0 {
|
if attachment.IssueID == 0 || attachment.CommentID != 0 {
|
||||||
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
|
log.Debug("Requested attachment[%d] is not an issue attachment.", attachment.ID)
|
||||||
ctx.APIErrorNotFound("no such attachment in issue")
|
ctx.APIErrorNotFound("no such attachment in issue")
|
||||||
return false
|
return false
|
||||||
} else if issue != nil && attachment.IssueID != issue.ID {
|
} else if issue != nil && attachment.IssueID != issue.ID {
|
||||||
|
|||||||
@@ -291,6 +291,11 @@ func AddPushMirror(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if setting.Mirror.DisableNewPush {
|
||||||
|
ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new push mirrors")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
pushMirror := web.GetForm[*api.CreatePushMirrorOption](ctx)
|
pushMirror := web.GetForm[*api.CreatePushMirrorOption](ctx)
|
||||||
CreatePushMirror(ctx, pushMirror)
|
CreatePushMirror(ctx, pushMirror)
|
||||||
}
|
}
|
||||||
@@ -356,7 +361,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
|
|||||||
|
|
||||||
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
|
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
|
err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleRemoteAddressError(ctx, err)
|
HandleRemoteAddressError(ctx, err)
|
||||||
|
|||||||
@@ -10,13 +10,36 @@ import (
|
|||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/test"
|
"gitea.dev/modules/test"
|
||||||
"gitea.dev/services/contexttest"
|
"gitea.dev/services/contexttest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCreatePushMirrorUsesCallerPermission(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.ImportLocalPaths, true)()
|
||||||
|
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
|
||||||
|
ctx.Doer = &user_model.User{}
|
||||||
|
ctx.ContextUser = &user_model.User{AllowImportLocal: true}
|
||||||
|
|
||||||
|
CreatePushMirror(ctx, &api.CreatePushMirrorOption{RemoteAddress: "local-mirror", Interval: "0"})
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPushMirrorDisabled(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Mirror.DisableNewPush, true)()
|
||||||
|
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
|
||||||
|
|
||||||
|
AddPushMirror(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, resp.Code)
|
||||||
|
assert.Contains(t, resp.Body.String(), "the site administrator has disabled the creation of new push mirrors")
|
||||||
|
}
|
||||||
|
|
||||||
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
|
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
|
||||||
// of aborting on the first failure, reporting all failed remotes with a 422.
|
// of aborting on the first failure, reporting all failed remotes with a 422.
|
||||||
// Each remote name is not a configured git remote, so SyncPushMirror fails fast
|
// Each remote name is not a configured git remote, so SyncPushMirror fails fast
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts
|
|||||||
// The repo is empty and being initialized by this push, so there is no
|
// The repo is empty and being initialized by this push, so there is no
|
||||||
// dependent state (webhooks, notifications, visibility fan-out) to reconcile
|
// dependent state (webhooks, notifications, visibility fan-out) to reconcile
|
||||||
// yet; setting the flags directly is sufficient in this push-to-create case.
|
// yet; setting the flags directly is sufficient in this push-to-create case.
|
||||||
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
|
if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() &&
|
||||||
|
(isPrivate.Value() || !setting.Repository.ForcePrivate || ctx.Doer.IsAdmin) {
|
||||||
repo.IsPrivate = isPrivate.Value()
|
repo.IsPrivate = isPrivate.Value()
|
||||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
|
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
|
||||||
log.Error("failed to update repo is_private: %v", err)
|
log.Error("failed to update repo is_private: %v", err)
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ const (
|
|||||||
// UserSearchDefaultAdminSort is the default sort type for admin view
|
// UserSearchDefaultAdminSort is the default sort type for admin view
|
||||||
const UserSearchDefaultAdminSort = "alphabetically"
|
const UserSearchDefaultAdminSort = "alphabetically"
|
||||||
|
|
||||||
|
// authSourceFilterOption is one radio item of the authentication source filter dropdown
|
||||||
|
type authSourceFilterOption struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
// Users show all the users
|
// Users show all the users
|
||||||
func Users(ctx *context.Context) {
|
func Users(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("admin.users")
|
ctx.Data["Title"] = ctx.Tr("admin.users")
|
||||||
@@ -76,6 +83,30 @@ func Users(ctx *context.Context) {
|
|||||||
"SortType": sortType,
|
"SortType": sortType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inactive sources are listed too, users stay attached to a source after it is deactivated
|
||||||
|
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("auth.Sources", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceIDFilter := ctx.FormOptionalInt64("source_id")
|
||||||
|
sourceNames := make(map[int64]string, len(sources))
|
||||||
|
authSourceFilterOptions := []*authSourceFilterOption{
|
||||||
|
{Value: "", Label: ctx.Locale.TrString("all"), Selected: !sourceIDFilter.Has()},
|
||||||
|
{Value: "0", Label: ctx.Locale.TrString("admin.users.local"), Selected: sourceIDFilter.Has() && sourceIDFilter.Value() == 0},
|
||||||
|
}
|
||||||
|
for _, source := range sources {
|
||||||
|
sourceNames[source.ID] = source.Name
|
||||||
|
authSourceFilterOptions = append(authSourceFilterOptions, &authSourceFilterOption{
|
||||||
|
Value: strconv.FormatInt(source.ID, 10),
|
||||||
|
Label: source.Name,
|
||||||
|
Selected: sourceIDFilter.Has() && sourceIDFilter.Value() == source.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx.Data["HasAuthSources"] = len(sources) > 0
|
||||||
|
ctx.Data["SourceNames"] = sourceNames
|
||||||
|
ctx.Data["AuthSourceFilterOptions"] = authSourceFilterOptions
|
||||||
|
|
||||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Types: types,
|
Types: types,
|
||||||
@@ -88,6 +119,7 @@ func Users(ctx *context.Context) {
|
|||||||
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
||||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||||
|
SourceID: sourceIDFilter,
|
||||||
OrderBy: db.SearchOrderBy(sortType),
|
OrderBy: db.SearchOrderBy(sortType),
|
||||||
}, tplUsers)
|
}, tplUsers)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
|
|||||||
}
|
}
|
||||||
|
|
||||||
token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
|
token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
|
||||||
if err != nil {
|
if err != nil || token.Kind != oauth2_provider.KindRefreshToken {
|
||||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
|
ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
|
||||||
ErrorDescription: "unable to parse refresh token",
|
ErrorDescription: "unable to parse refresh token",
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
activities_model "gitea.dev/models/activities"
|
activities_model "gitea.dev/models/activities"
|
||||||
"gitea.dev/models/organization"
|
"gitea.dev/models/organization"
|
||||||
"gitea.dev/models/renderhelper"
|
"gitea.dev/models/renderhelper"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/markup/markdown"
|
"gitea.dev/modules/markup/markdown"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/services/context"
|
"gitea.dev/services/context"
|
||||||
feed_service "gitea.dev/services/feed"
|
feed_service "gitea.dev/services/feed"
|
||||||
|
|
||||||
@@ -28,8 +30,15 @@ func ShowUserFeedAtom(ctx *context.Context) {
|
|||||||
|
|
||||||
// showUserFeed show user activity as RSS / Atom feed
|
// showUserFeed show user activity as RSS / Atom feed
|
||||||
func showUserFeed(ctx *context.Context, formatType string) {
|
func showUserFeed(ctx *context.Context, formatType string) {
|
||||||
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
|
||||||
isOrganisation := ctx.ContextUser.IsOrganization()
|
isOrganisation := ctx.ContextUser.IsOrganization()
|
||||||
|
if !setting.Other.EnableFeed ||
|
||||||
|
isOrganisation && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
|
||||||
|
!isOrganisation && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||||
if ctx.IsSigned && isOrganisation && !includePrivate {
|
if ctx.IsSigned && isOrganisation && !includePrivate {
|
||||||
// When feed is requested by a member of the organization,
|
// When feed is requested by a member of the organization,
|
||||||
// include the private repo's the member has access to.
|
// include the private repo's the member has access to.
|
||||||
|
|||||||
@@ -642,7 +642,15 @@ func (data *actionRunListData) preparePartialRefreshRuns(ctx *context.Context) b
|
|||||||
ctx.ServerError("GetRunsByRepoAndID", err)
|
ctx.ServerError("GetRunsByRepoAndID", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
data.ActionRuns = runs
|
runsMap := make(map[int64]*actions_model.ActionRun, len(runs))
|
||||||
|
for _, run := range runs {
|
||||||
|
runsMap[run.ID] = run
|
||||||
|
}
|
||||||
|
for _, id := range data.refreshRunIDs {
|
||||||
|
if run, ok := runsMap[id]; ok {
|
||||||
|
data.ActionRuns = append(data.ActionRuns, run)
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/test"
|
"gitea.dev/modules/test"
|
||||||
web_context "gitea.dev/services/context"
|
web_context "gitea.dev/services/context"
|
||||||
|
"gitea.dev/services/contexttest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -74,3 +75,17 @@ func newWorkflowBadgeTestContext(t *testing.T) *web_context.Context {
|
|||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestActionRunListData(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
t.Run("preparePartialRefreshRuns", func(t *testing.T) {
|
||||||
|
ctx, _ := contexttest.MockContext(t, "user5/repo4/actions")
|
||||||
|
contexttest.LoadRepo(t, ctx, 4)
|
||||||
|
d := &actionRunListData{refreshRunIDs: []int64{791, 792}}
|
||||||
|
d.preparePartialRefreshRuns(ctx)
|
||||||
|
assert.Equal(t, []int64{791, 792}, []int64{d.ActionRuns[0].ID, d.ActionRuns[1].ID})
|
||||||
|
d = &actionRunListData{refreshRunIDs: []int64{792, 791}}
|
||||||
|
d.preparePartialRefreshRuns(ctx)
|
||||||
|
assert.Equal(t, []int64{792, 791}, []int64{d.ActionRuns[0].ID, d.ActionRuns[1].ID})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -660,6 +660,11 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isDelTag && !rel.IsTag {
|
||||||
|
ctx.HTTPError(http.StatusConflict, "a tag attached to a release cannot be deleted directly")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil {
|
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil {
|
||||||
if release_service.IsErrProtectedTagName(err) {
|
if release_service.IsErrProtectedTagName(err) {
|
||||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -21,6 +22,21 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestDeleteTagRetainsReleaseAndAttachments(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
ctx, resp := contexttest.MockContext(t, "POST user2/repo1/tags/delete?id=1")
|
||||||
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
|
||||||
|
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9, ReleaseID: 1})
|
||||||
|
|
||||||
|
DeleteTag(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusConflict, resp.Code)
|
||||||
|
assert.Equal(t, release, unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1}))
|
||||||
|
assert.Equal(t, attachment, unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9}))
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewReleasePost(t *testing.T) {
|
func TestNewReleasePost(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
|
|
||||||
|
|||||||
@@ -734,18 +734,10 @@ func UsernameSubRoute(ctx *context.Context) {
|
|||||||
ShowGPGKeys(ctx)
|
ShowGPGKeys(ctx)
|
||||||
}
|
}
|
||||||
case strings.HasSuffix(username, ".rss"):
|
case strings.HasSuffix(username, ".rss"):
|
||||||
if !setting.Other.EnableFeed {
|
|
||||||
ctx.HTTPError(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if reloadParam(".rss") {
|
if reloadParam(".rss") {
|
||||||
feed.ShowUserFeedRSS(ctx)
|
feed.ShowUserFeedRSS(ctx)
|
||||||
}
|
}
|
||||||
case strings.HasSuffix(username, ".atom"):
|
case strings.HasSuffix(username, ".atom"):
|
||||||
if !setting.Other.EnableFeed {
|
|
||||||
ctx.HTTPError(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if reloadParam(".atom") {
|
if reloadParam(".atom") {
|
||||||
feed.ShowUserFeedAtom(ctx)
|
feed.ShowUserFeedAtom(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,6 +322,13 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
|||||||
|
|
||||||
// ActionUserFollow is for follow/unfollow user request
|
// ActionUserFollow is for follow/unfollow user request
|
||||||
func ActionUserFollow(ctx *context.Context) {
|
func ActionUserFollow(ctx *context.Context) {
|
||||||
|
isOrg := ctx.ContextUser.IsOrganization()
|
||||||
|
if isOrg && !organization.HasOrgOrUserVisible(ctx, ctx.ContextUser, ctx.Doer) ||
|
||||||
|
!isOrg && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
switch ctx.FormString("action") {
|
switch ctx.FormString("action") {
|
||||||
case "follow":
|
case "follow":
|
||||||
|
|||||||
@@ -107,9 +107,8 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info("Unable to get global API settings. Ignoring these.")
|
log.Info("Unable to get global API settings. Ignoring these.")
|
||||||
log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err)
|
log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err)
|
||||||
}
|
} else if apiConf != nil && apiConf.MaxResponseItems > 0 {
|
||||||
if apiConf != nil {
|
maxPerPage = min(apiConf.MaxResponseItems, 100)
|
||||||
maxPerPage = apiConf.MaxResponseItems
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &GiteaDownloader{
|
return &GiteaDownloader{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package migrations
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@@ -317,15 +318,16 @@ func TestGiteaDownloadRepo(t *testing.T) {
|
|||||||
|
|
||||||
func TestGiteaDownloadCommentsPaging(t *testing.T) {
|
func TestGiteaDownloadCommentsPaging(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
maxResponseItems, commentCount, requests int
|
maxResponseItems, pageSize, commentCount, requests int
|
||||||
paginated bool
|
paginated bool
|
||||||
}{
|
}{
|
||||||
{maxResponseItems: 2, commentCount: 2, requests: 2},
|
{maxResponseItems: 2, pageSize: 2, commentCount: 2, requests: 2},
|
||||||
{maxResponseItems: 2, commentCount: 3, requests: 1},
|
{maxResponseItems: 2, pageSize: 2, commentCount: 3, requests: 1},
|
||||||
{maxResponseItems: 2, commentCount: 4, requests: 3, paginated: true},
|
{maxResponseItems: 2, pageSize: 2, commentCount: 4, requests: 3, paginated: true},
|
||||||
{maxResponseItems: 0, commentCount: 0, requests: 1},
|
{maxResponseItems: 0, pageSize: 10, commentCount: 0, requests: 1},
|
||||||
|
{maxResponseItems: math.MaxInt, pageSize: 100, commentCount: 0, requests: 1},
|
||||||
} {
|
} {
|
||||||
t.Run(strconv.Itoa(tc.commentCount), func(t *testing.T) {
|
t.Run(fmt.Sprintf("maxResponseItems=%d/comments=%d", tc.maxResponseItems, tc.commentCount), func(t *testing.T) {
|
||||||
commentRequests := 0
|
commentRequests := 0
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@@ -352,6 +354,7 @@ func TestGiteaDownloadCommentsPaging(t *testing.T) {
|
|||||||
|
|
||||||
downloader, err := NewGiteaDownloader(t.Context(), server.URL, "o/r", "", "", "")
|
downloader, err := NewGiteaDownloader(t.Context(), server.URL, "o/r", "", "", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.pageSize, downloader.maxPerPage)
|
||||||
|
|
||||||
comments, _, err := downloader.GetComments(t.Context(), &base.Issue{Number: 1})
|
comments, _, err := downloader.GetComments(t.Context(), &base.Issue{Number: 1})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -47,6 +47,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Authentication Source Filter Menu Item -->
|
||||||
|
{{if .HasAuthSources}}
|
||||||
|
<div class="ui dropdown type jump item">
|
||||||
|
<span class="text">{{ctx.Locale.Tr "admin.users.auth_source"}}</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu flex-items-menu">
|
||||||
|
{{range $index, $option := .AuthSourceFilterOptions}}
|
||||||
|
{{if eq $index 1}}<div class="divider"></div>{{end}}
|
||||||
|
<label class="item"><input type="radio" name="source_id" value="{{$option.Value}}" {{if $option.Selected}}checked{{end}}> {{$option.Label}}</label>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- Sort Menu Item -->
|
<!-- Sort Menu Item -->
|
||||||
<div class="ui dropdown type jump item">
|
<div class="ui dropdown type jump item">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
@@ -75,6 +89,7 @@
|
|||||||
{{SortArrow "alphabetically" "reversealphabetically" $.SortType true}}
|
{{SortArrow "alphabetically" "reversealphabetically" $.SortType true}}
|
||||||
</th>
|
</th>
|
||||||
<th>{{ctx.Locale.Tr "email"}}</th>
|
<th>{{ctx.Locale.Tr "email"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "admin.users.auth_source"}}</th>
|
||||||
<th>{{ctx.Locale.Tr "admin.users.activated"}}</th>
|
<th>{{ctx.Locale.Tr "admin.users.activated"}}</th>
|
||||||
<th>{{ctx.Locale.Tr "admin.users.restricted"}}</th>
|
<th>{{ctx.Locale.Tr "admin.users.restricted"}}</th>
|
||||||
<th>{{ctx.Locale.Tr "admin.users.2fa"}}</th>
|
<th>{{ctx.Locale.Tr "admin.users.2fa"}}</th>
|
||||||
@@ -102,6 +117,7 @@
|
|||||||
{{template "shared/user/user_type_label" .}}
|
{{template "shared/user/user_type_label" .}}
|
||||||
</td>
|
</td>
|
||||||
<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
|
<td class="gt-ellipsis tw-max-w-48">{{.Email}}</td>
|
||||||
|
<td class="gt-ellipsis tw-max-w-32">{{if .LoginSource}}{{index $.SourceNames .LoginSource}}{{else}}{{ctx.Locale.Tr "admin.users.local"}}{{end}}</td>
|
||||||
<td>{{svg (Iif .IsActive "octicon-check" "octicon-x")}}</td>
|
<td>{{svg (Iif .IsActive "octicon-check" "octicon-x")}}</td>
|
||||||
<td>{{svg (Iif .IsRestricted "octicon-check" "octicon-x")}}</td>
|
<td>{{svg (Iif .IsRestricted "octicon-check" "octicon-x")}}</td>
|
||||||
<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
|
<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
|
||||||
@@ -119,7 +135,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr class="no-results-row"><td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
<tr class="no-results-row"><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+1
-1
@@ -11939,7 +11939,7 @@
|
|||||||
"operationId": "adminSearchUsers",
|
"operationId": "adminSearchUsers",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "ID of the user's login source to search for",
|
"description": "ID of the user's login source to search for, 0 means the local users",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"name": "source_id",
|
"name": "source_id",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|||||||
+1
-1
@@ -825,7 +825,7 @@
|
|||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"description": "ID of the user's login source to search for",
|
"description": "ID of the user's login source to search for, 0 means the local users",
|
||||||
"name": "source_id",
|
"name": "source_id",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ test('create a bot and manage its access token', async ({page, request}) => {
|
|||||||
await page.getByRole('row', {name: /^user /}).getByRole('radio', {name: 'Read', exact: true}).check();
|
await page.getByRole('row', {name: /^user /}).getByRole('radio', {name: 'Read', exact: true}).check();
|
||||||
await page.getByRole('button', {name: 'Generate Token'}).click();
|
await page.getByRole('button', {name: 'Generate Token'}).click();
|
||||||
const token = await page.getByRole('code').textContent();
|
const token = await page.getByRole('code').textContent();
|
||||||
|
await page.getByRole('button', {name: 'Copy', exact: true}).click();
|
||||||
|
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(token);
|
||||||
const response = await request.get('/api/v1/user', {headers: {Authorization: `token ${token}`}});
|
const response = await request.get('/api/v1/user', {headers: {Authorization: `token ${token}`}});
|
||||||
expect(await response.json()).toMatchObject({login: botName, type: 'Bot'});
|
expect(await response.json()).toMatchObject({login: botName, type: 'Bot'});
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ test('pdf file', async ({page, request}) => {
|
|||||||
await assertFlushWithParent(container, page.locator('.file-view'));
|
await assertFlushWithParent(container, page.locator('.file-view'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('code line anchors', async ({page, request}) => {
|
||||||
|
const repoName = `e2e-line-anchor-${randomString(8)}`;
|
||||||
|
const owner = env.GITEA_TEST_E2E_USER;
|
||||||
|
await apiCreateRepo(request, {name: repoName});
|
||||||
|
await apiCreateFiles(request, owner, repoName, [{path: 'test.txt', content: 'a\n'}]);
|
||||||
|
const url = `/${owner}/${repoName}/src/branch/main/test.txt`;
|
||||||
|
await page.goto(`${url}#L0`);
|
||||||
|
await page.goto(`${url}#L1`);
|
||||||
|
await expect(page.locator('.code-view tr.active')).toHaveCount(1);
|
||||||
|
await assertNoJsError(page);
|
||||||
|
});
|
||||||
|
|
||||||
test('asciicast file', async ({page, request}) => {
|
test('asciicast file', async ({page, request}) => {
|
||||||
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
||||||
const owner = env.GITEA_TEST_E2E_USER;
|
const owner = env.GITEA_TEST_E2E_USER;
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import (
|
|||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
api "gitea.dev/modules/structs"
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/test"
|
"gitea.dev/modules/test"
|
||||||
|
"gitea.dev/services/auth/source/ldap"
|
||||||
"gitea.dev/tests"
|
"gitea.dev/tests"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -34,6 +36,49 @@ func TestAdminViewUsers(t *testing.T) {
|
|||||||
session.MakeRequest(t, req, http.StatusForbidden)
|
session.MakeRequest(t, req, http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminViewUsersFilterAuthSource(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
source := &auth_model.Source{Type: auth_model.LDAP, Name: "test-user-list-filter", IsActive: false, Cfg: &ldap.Source{}} // users stay attached to a deactivated source
|
||||||
|
require.NoError(t, auth_model.CreateSource(t.Context(), source))
|
||||||
|
|
||||||
|
user2 := &user_model.User{ID: 2, LoginType: auth_model.LDAP, LoginSource: source.ID}
|
||||||
|
require.NoError(t, user_model.UpdateUserCols(t.Context(), user2, "login_type", "login_source"))
|
||||||
|
|
||||||
|
session := loginUser(t, "user1")
|
||||||
|
listUsers := func(query string) (*HTMLDoc, []string) {
|
||||||
|
req := NewRequest(t, "GET", "/-/admin/users?"+query)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
return doc, doc.Find("table tbody tr td:nth-child(2) a").Map(func(_ int, s *goquery.Selection) string {
|
||||||
|
return s.Text()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, users := listUsers("source_id=") // the "All" option submits an empty value
|
||||||
|
AssertHTMLElement(t, doc, `input[name="source_id"][value=""][checked]`, true)
|
||||||
|
assert.Subset(t, users, []string{"user1", "user2"})
|
||||||
|
|
||||||
|
doc, users = listUsers(fmt.Sprintf("source_id=%d", source.ID)) // the "test-user-list-filter" LDAP source
|
||||||
|
AssertHTMLElement(t, doc, fmt.Sprintf(`input[name="source_id"][value="%d"][checked]`, source.ID), true)
|
||||||
|
assert.Equal(t, []string{"user2"}, users)
|
||||||
|
assert.Equal(t, source.Name, doc.Find("table tbody tr td:nth-child(4)").Text())
|
||||||
|
|
||||||
|
_, users = listUsers("source_id=0") // 0 means the "Local" source
|
||||||
|
assert.Contains(t, users, "user1")
|
||||||
|
assert.NotContains(t, users, "user2")
|
||||||
|
|
||||||
|
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadAdmin)
|
||||||
|
req := NewRequest(t, "GET", "/api/v1/admin/users?source_id=0").AddTokenAuth(token) // the API also treats 0 as local users
|
||||||
|
apiUsers := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), []api.User{})
|
||||||
|
apiUserNames := make([]string, 0, len(apiUsers))
|
||||||
|
for _, u := range apiUsers {
|
||||||
|
apiUserNames = append(apiUserNames, u.UserName)
|
||||||
|
}
|
||||||
|
assert.Contains(t, apiUserNames, "user1")
|
||||||
|
assert.NotContains(t, apiUserNames, "user2")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAdminViewUser(t *testing.T) {
|
func TestAdminViewUser(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ func TestAPIGetIssueAttachment(t *testing.T) {
|
|||||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||||
|
|
||||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
||||||
|
|
||||||
|
commentAttachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 3, RepoID: repo.ID})
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: commentAttachment.IssueID}).Index, commentAttachment.ID)).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIListIssueAttachments(t *testing.T) {
|
func TestAPIListIssueAttachments(t *testing.T) {
|
||||||
|
|||||||
@@ -170,8 +170,9 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
rootPaths := []string{
|
rootPaths := []string{
|
||||||
fmt.Sprintf("/api/packages/%s/npm/@scope/test-package", user.Name),
|
"/api/packages/user2/npm/@scope/test-package",
|
||||||
fmt.Sprintf("/api/packages/%s/npm/@scope%%2ftest-package", user.Name),
|
"/api/packages/user2/npm/@scope%2Ftest-package",
|
||||||
|
"/api/packages/user2/npm/%40scope%2ftest-package",
|
||||||
}
|
}
|
||||||
for _, root := range rootPaths {
|
for _, root := range rootPaths {
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)).AddTokenAuth(token)
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename)).AddTokenAuth(token)
|
||||||
@@ -186,7 +187,7 @@ func TestPackageNpm(t *testing.T) {
|
|||||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm)
|
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, pvs, 1)
|
assert.Len(t, pvs, 1)
|
||||||
assert.Equal(t, int64(4), pvs[0].DownloadCount)
|
assert.EqualValues(t, 6, pvs[0].DownloadCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("PackageMetadata", func(t *testing.T) {
|
t.Run("PackageMetadata", func(t *testing.T) {
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
|
|||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
|
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo2/media/test.xml").AddTokenAuth(getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository))
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
|
|
||||||
// Test with LFS
|
// Test with LFS
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
createLFSTestRepository(t, "repo-lfs-test")
|
createLFSTestRepository(t, "repo-lfs-test")
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
repo_service "gitea.dev/services/repository"
|
repo_service "gitea.dev/services/repository"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -175,6 +177,14 @@ func TestGitPushVisibilityOption(t *testing.T) {
|
|||||||
doGitPushTestRepository(gitPath, "origin", "branch2", "-o", "repo.private=false")(t)
|
doGitPushTestRepository(gitPath, "origin", "branch2", "-o", "repo.private=false")(t)
|
||||||
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
|
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID})
|
||||||
assert.True(t, repo.IsPrivate, "repo.private option must be ignored on an existing repository")
|
assert.True(t, repo.IsPrivate, "repo.private option must be ignored on an existing repository")
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Repository.ForcePrivate, true)()
|
||||||
|
forcedRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{Name: "repo-visibility-forced", DefaultBranch: "master", IsPrivate: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
u.Path = forcedRepo.FullName() + ".git"
|
||||||
|
doGitAddRemote(gitPath, "forced", u)(t)
|
||||||
|
doGitPushTestRepository(gitPath, "forced", "master", "-o", "repo.private=false")(t)
|
||||||
|
assert.True(t, unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: forcedRepo.ID}).IsPrivate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -571,6 +571,15 @@ func testRefreshTokenInvalidation(t *testing.T) {
|
|||||||
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
|
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
|
||||||
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
|
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||||
|
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||||
|
"redirect_uri": "https://example.com",
|
||||||
|
"refresh_token": parsed.AccessToken,
|
||||||
|
})
|
||||||
|
MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
|
||||||
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||||
"grant_type": "refresh_token",
|
"grant_type": "refresh_token",
|
||||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
|||||||
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
||||||
req := NewRequest(t, "GET", "/"+org22.Name)
|
req := NewRequest(t, "GET", "/"+org22.Name)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", "/"+org22.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
|
||||||
|
|
||||||
session := loginUser(t, "user1")
|
session := loginUser(t, "user1")
|
||||||
oldName := org22.Name
|
oldName := org22.Name
|
||||||
@@ -106,6 +107,8 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
|||||||
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
|
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
|
||||||
req = NewRequest(t, "GET", "/"+org23.Name)
|
req = NewRequest(t, "GET", "/"+org23.Name)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
strangerSession := loginUser(t, "user4")
|
||||||
|
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+org23.Name+"?action=follow"), http.StatusNotFound)
|
||||||
|
|
||||||
oldName = org23.Name
|
oldName = org23.Name
|
||||||
newName = "org23_renamed"
|
newName = "org23_renamed"
|
||||||
@@ -127,6 +130,8 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
|||||||
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
|
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
|
||||||
req = NewRequest(t, "GET", "/"+user31.Name)
|
req = NewRequest(t, "GET", "/"+user31.Name)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
MakeRequest(t, NewRequest(t, "GET", "/"+user31.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
|
||||||
|
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+user31.Name+"?action=follow"), http.StatusNotFound)
|
||||||
|
|
||||||
oldName = user31.Name
|
oldName = user31.Name
|
||||||
newName = "user31_renamed"
|
newName = "user31_renamed"
|
||||||
@@ -330,6 +335,10 @@ func testGetUserRss(t *testing.T) {
|
|||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
req = NewRequestf(t, "GET", "/non-existent-user.rss")
|
req = NewRequestf(t, "GET", "/non-existent-user.rss")
|
||||||
session.MakeRequest(t, req, http.StatusNotFound)
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Other.EnableFeed, false)()
|
||||||
|
MakeRequest(t, NewRequestf(t, "GET", "/%s.rss", user34), http.StatusNotFound)
|
||||||
|
MakeRequest(t, NewRequestf(t, "GET", "/%s", user34).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUserListStopWatches(t *testing.T) {
|
func testUserListStopWatches(t *testing.T) {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ function selectRange(range: string): Element | null {
|
|||||||
stopLineNum = tmp;
|
stopLineNum = tmp;
|
||||||
range = `${stop}-${start}`;
|
range = `${stop}-${start}`;
|
||||||
}
|
}
|
||||||
|
if (startLineNum < 1) return null;
|
||||||
|
|
||||||
const first = elLineNums[startLineNum - 1] ?? null;
|
const first = elLineNums[startLineNum - 1] ?? null;
|
||||||
for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) {
|
for (let i = startLineNum - 1; i <= stopLineNum - 1 && i < elLineNums.length; i++) {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function replaceWithFeedbackSvg(origSvg: SVGElement, success: boolean): () => vo
|
|||||||
|
|
||||||
// Enable clipboard copy from HTML attributes. These properties are supported:
|
// Enable clipboard copy from HTML attributes. These properties are supported:
|
||||||
// - data-clipboard-text: Direct text to copy
|
// - data-clipboard-text: Direct text to copy
|
||||||
// - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of <div> will be copied
|
// - data-clipboard-target: Holds a selector for an element. "value" of <input> or <textarea>, or "textContent" of other elements will be copied
|
||||||
export function initGlobalCopyToClipboardListener() {
|
export function initGlobalCopyToClipboardListener() {
|
||||||
document.addEventListener('click', async (e) => {
|
document.addEventListener('click', async (e) => {
|
||||||
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-clipboard-text], [data-clipboard-target]');
|
const target = (e.target as HTMLElement).closest<HTMLElement>('[data-clipboard-text], [data-clipboard-target]');
|
||||||
@@ -78,10 +78,8 @@ export function initGlobalCopyToClipboardListener() {
|
|||||||
const textTarget = document.querySelector(textSelector)!;
|
const textTarget = document.querySelector(textSelector)!;
|
||||||
if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') {
|
if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') {
|
||||||
text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value;
|
text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value;
|
||||||
} else if (textTarget.nodeName === 'DIV') {
|
|
||||||
text = textTarget.textContent;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported element for clipboard target: ${textSelector}`);
|
text = textTarget.textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// now, text can not be null
|
// now, text can not be null
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ test('isGiteaError', () => {
|
|||||||
expect(isGiteaError('', `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
expect(isGiteaError('', `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
||||||
expect(isGiteaError('', `Error\n at https://other-site.com/script.js:1:1`)).toBe(false);
|
expect(isGiteaError('', `Error\n at https://other-site.com/script.js:1:1`)).toBe(false);
|
||||||
expect(isGiteaError('', `Error\n at ${origin}/assets/js/index.abc123.js:1:1`)).toBe(true);
|
expect(isGiteaError('', `Error\n at ${origin}/assets/js/index.abc123.js:1:1`)).toBe(true);
|
||||||
|
expect(isGiteaError('', `Error\n at ${origin}/web_src/js/index.ts:1:1`)).toBe(false);
|
||||||
expect(isGiteaError(`${origin}/assets/js/index.js`, `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
expect(isGiteaError(`${origin}/assets/js/index.js`, `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false);
|
||||||
|
vi.spyOn(window.config, 'runModeIsProd', 'get').mockReturnValue(false);
|
||||||
|
expect(isGiteaError('', `Error\n at ${origin}/web_src/js/index.ts:1:1`)).toBe(true);
|
||||||
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('showGlobalErrorMessage', () => {
|
test('showGlobalErrorMessage', () => {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function isGiteaError(filename: string, stack: string): boolean {
|
|||||||
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
||||||
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
|
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
|
||||||
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
|
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
|
||||||
|
if (!windowConfig()?.runModeIsProd && stack.includes(`${window.location.origin}/web_src/`)) return true;
|
||||||
return !stack || stack.includes(assetBaseUrl);
|
return !stack || stack.includes(assetBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user