mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-01 20:59:45 +09:00
Merge branch 'main' into copilot/fix-mathml-font-issue
This commit is contained in:
@@ -198,7 +198,7 @@
|
||||
;; 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
|
||||
;; 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
|
||||
;SSH_AUTHORIZED_KEYS_BACKUP = false
|
||||
@@ -237,7 +237,7 @@
|
||||
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s
|
||||
;;
|
||||
;; 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
|
||||
;; (Other common TLS configuration are found before)
|
||||
@@ -1669,13 +1669,13 @@ LEVEL = Info
|
||||
;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; General queue queue type, currently support: persistable-channel, channel, level, redis, dummy
|
||||
;; default to persistable-channel
|
||||
;TYPE = persistable-channel
|
||||
;; General queue type, currently support: level, channel, redis, dummy
|
||||
;; default to level
|
||||
;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"
|
||||
;DATADIR = queues/
|
||||
;DATADIR = queues/common
|
||||
;;
|
||||
;; Default queue length before a channel queue will block
|
||||
;LENGTH = 100000
|
||||
@@ -1683,7 +1683,7 @@ LEVEL = Info
|
||||
;; Batch size to send for batched queues
|
||||
;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`.
|
||||
;; When `TYPE` is `redis` and this is left empty, it falls back to the shared [redis] CONN_STR.
|
||||
;CONN_STR =
|
||||
|
||||
@@ -40,8 +40,8 @@ type SearchUserOptions struct {
|
||||
Keyword string
|
||||
Types []UserType
|
||||
UID int64
|
||||
LoginName string // this option should be used only for admin user
|
||||
SourceID int64 // this option should be used only for admin user
|
||||
LoginName string // 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
|
||||
Visible []structs.VisibleType
|
||||
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})
|
||||
}
|
||||
|
||||
if opts.SourceID > 0 {
|
||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
|
||||
if opts.SourceID.Has() {
|
||||
cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()})
|
||||
}
|
||||
if opts.LoginName != "" {
|
||||
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
type htmlChunkReader struct {
|
||||
@@ -30,6 +32,10 @@ type escapeStreamer struct {
|
||||
ambiguousTables []*AmbiguousTable
|
||||
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
|
||||
}
|
||||
|
||||
@@ -62,6 +68,7 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
||||
for i, part := range parts {
|
||||
if partInTag[i] {
|
||||
lastIsTag = true
|
||||
es.trackHtmlTag(part)
|
||||
if _, err := out.Write(part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -75,7 +82,11 @@ func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts .
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom)
|
||||
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>`,
|
||||
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) {
|
||||
@@ -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) {
|
||||
defer test.MockVariableValue(&setting.UI.AmbiguousUnicodeDetection, true)()
|
||||
_, 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"
|
||||
// if we don't stop it, it will go into the TextNode again and create an infinite recursion
|
||||
return node.NextSibling
|
||||
} else if node.Data == "code" || node.Data == "pre" {
|
||||
return node.NextSibling // ignore code and pre nodes
|
||||
} else if node.Data == "code" || node.Data == "pre" || node.Data == "math" {
|
||||
return node.NextSibling // ignore code, pre and math nodes
|
||||
} else if node.Data == "img" {
|
||||
return visitNodeImg(ctx, node)
|
||||
} 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`)
|
||||
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>`)
|
||||
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
|
||||
test("<script>a", `<script>a`)
|
||||
|
||||
@@ -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
|
||||
// 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) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ func SearchUsers(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: source_id
|
||||
// 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
|
||||
// format: int64
|
||||
// - name: login_name
|
||||
@@ -483,7 +483,7 @@ func SearchUsers(ctx *context.APIContext) {
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
LoginName: ctx.FormTrim("login_name"),
|
||||
SourceID: ctx.FormInt64("source_id"),
|
||||
SourceID: ctx.FormOptionalInt64("source_id"),
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
Visible: visible,
|
||||
OrderBy: orderBy,
|
||||
|
||||
@@ -162,7 +162,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
||||
|
||||
// if it's not a pointer, just serve the data directly
|
||||
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
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
||||
|
||||
// If there isn't one, just serve the data directly
|
||||
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
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -198,7 +198,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -386,8 +386,8 @@ func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_mo
|
||||
ctx.APIErrorNotFound("no such attachment in repo")
|
||||
return false
|
||||
}
|
||||
if attachment.IssueID == 0 {
|
||||
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
|
||||
if attachment.IssueID == 0 || attachment.CommentID != 0 {
|
||||
log.Debug("Requested attachment[%d] is not an issue attachment.", attachment.ID)
|
||||
ctx.APIErrorNotFound("no such attachment in issue")
|
||||
return false
|
||||
} else if issue != nil && attachment.IssueID != issue.ID {
|
||||
|
||||
@@ -291,6 +291,11 @@ func AddPushMirror(ctx *context.APIContext) {
|
||||
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)
|
||||
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)
|
||||
if err == nil {
|
||||
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
|
||||
err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
|
||||
}
|
||||
if err != nil {
|
||||
HandleRemoteAddressError(ctx, err)
|
||||
|
||||
@@ -10,13 +10,36 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"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
|
||||
// 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
|
||||
|
||||
@@ -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
|
||||
// dependent state (webhooks, notifications, visibility fan-out) to reconcile
|
||||
// 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()
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
|
||||
log.Error("failed to update repo is_private: %v", err)
|
||||
|
||||
@@ -48,6 +48,13 @@ const (
|
||||
// UserSearchDefaultAdminSort is the default sort type for admin view
|
||||
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
|
||||
func Users(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.users")
|
||||
@@ -76,6 +83,30 @@ func Users(ctx *context.Context) {
|
||||
"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{
|
||||
Actor: ctx.Doer,
|
||||
Types: types,
|
||||
@@ -88,6 +119,7 @@ func Users(ctx *context.Context) {
|
||||
IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
|
||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||
SourceID: sourceIDFilter,
|
||||
OrderBy: db.SearchOrderBy(sortType),
|
||||
}, tplUsers)
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
|
||||
}
|
||||
|
||||
token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
|
||||
if err != nil {
|
||||
if err != nil || token.Kind != oauth2_provider.KindRefreshToken {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
|
||||
ErrorDescription: "unable to parse refresh token",
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/renderhelper"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
feed_service "gitea.dev/services/feed"
|
||||
|
||||
@@ -28,8 +30,15 @@ func ShowUserFeedAtom(ctx *context.Context) {
|
||||
|
||||
// showUserFeed show user activity as RSS / Atom feed
|
||||
func showUserFeed(ctx *context.Context, formatType string) {
|
||||
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
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 {
|
||||
// When feed is requested by a member of the organization,
|
||||
// include the private repo's the member has access to.
|
||||
|
||||
@@ -660,6 +660,11 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
||||
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 release_service.IsErrProtectedTagName(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
@@ -21,6 +22,21 @@ import (
|
||||
"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) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
|
||||
@@ -734,18 +734,10 @@ func UsernameSubRoute(ctx *context.Context) {
|
||||
ShowGPGKeys(ctx)
|
||||
}
|
||||
case strings.HasSuffix(username, ".rss"):
|
||||
if !setting.Other.EnableFeed {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if reloadParam(".rss") {
|
||||
feed.ShowUserFeedRSS(ctx)
|
||||
}
|
||||
case strings.HasSuffix(username, ".atom"):
|
||||
if !setting.Other.EnableFeed {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if reloadParam(".atom") {
|
||||
feed.ShowUserFeedAtom(ctx)
|
||||
}
|
||||
|
||||
@@ -322,6 +322,13 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
|
||||
// ActionUserFollow is for follow/unfollow user request
|
||||
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
|
||||
switch ctx.FormString("action") {
|
||||
case "follow":
|
||||
|
||||
@@ -107,9 +107,8 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
|
||||
if err != nil {
|
||||
log.Info("Unable to get global API settings. Ignoring these.")
|
||||
log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err)
|
||||
}
|
||||
if apiConf != nil {
|
||||
maxPerPage = apiConf.MaxResponseItems
|
||||
} else if apiConf != nil && apiConf.MaxResponseItems > 0 {
|
||||
maxPerPage = min(apiConf.MaxResponseItems, 100)
|
||||
}
|
||||
|
||||
return &GiteaDownloader{
|
||||
|
||||
@@ -5,6 +5,7 @@ package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -317,15 +318,16 @@ func TestGiteaDownloadRepo(t *testing.T) {
|
||||
|
||||
func TestGiteaDownloadCommentsPaging(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
maxResponseItems, commentCount, requests int
|
||||
paginated bool
|
||||
maxResponseItems, pageSize, commentCount, requests int
|
||||
paginated bool
|
||||
}{
|
||||
{maxResponseItems: 2, commentCount: 2, requests: 2},
|
||||
{maxResponseItems: 2, commentCount: 3, requests: 1},
|
||||
{maxResponseItems: 2, commentCount: 4, requests: 3, paginated: true},
|
||||
{maxResponseItems: 0, commentCount: 0, requests: 1},
|
||||
{maxResponseItems: 2, pageSize: 2, commentCount: 2, requests: 2},
|
||||
{maxResponseItems: 2, pageSize: 2, commentCount: 3, requests: 1},
|
||||
{maxResponseItems: 2, pageSize: 2, commentCount: 4, requests: 3, paginated: true},
|
||||
{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
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -352,6 +354,7 @@ func TestGiteaDownloadCommentsPaging(t *testing.T) {
|
||||
|
||||
downloader, err := NewGiteaDownloader(t.Context(), server.URL, "o/r", "", "", "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.pageSize, downloader.maxPerPage)
|
||||
|
||||
comments, _, err := downloader.GetComments(t.Context(), &base.Issue{Number: 1})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -47,6 +47,20 @@
|
||||
</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 -->
|
||||
<div class="ui dropdown type jump item">
|
||||
<span class="text">
|
||||
@@ -75,6 +89,7 @@
|
||||
{{SortArrow "alphabetically" "reversealphabetically" $.SortType true}}
|
||||
</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.restricted"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.users.2fa"}}</th>
|
||||
@@ -102,6 +117,7 @@
|
||||
{{template "shared/user/user_type_label" .}}
|
||||
</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 .IsRestricted "octicon-check" "octicon-x")}}</td>
|
||||
<td>{{svg (Iif (index $.UsersTwoFaStatus .ID) "octicon-check" "octicon-x")}}</td>
|
||||
@@ -119,7 +135,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{{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}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+1
-1
@@ -11939,7 +11939,7 @@
|
||||
"operationId": "adminSearchUsers",
|
||||
"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",
|
||||
"name": "source_id",
|
||||
"schema": {
|
||||
|
||||
+1
-1
@@ -825,7 +825,7 @@
|
||||
{
|
||||
"type": "integer",
|
||||
"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",
|
||||
"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('button', {name: 'Generate Token'}).click();
|
||||
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}`}});
|
||||
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'));
|
||||
});
|
||||
|
||||
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}) => {
|
||||
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/auth/source/ldap"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -34,6 +36,49 @@ func TestAdminViewUsers(t *testing.T) {
|
||||
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) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ func TestAPIGetIssueAttachment(t *testing.T) {
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
|
||||
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) {
|
||||
|
||||
@@ -22,6 +22,10 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
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
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
createLFSTestRepository(t, "repo-lfs-test")
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -175,6 +177,14 @@ func TestGitPushVisibilityOption(t *testing.T) {
|
||||
doGitPushTestRepository(gitPath, "origin", "branch2", "-o", "repo.private=false")(t)
|
||||
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")
|
||||
|
||||
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, "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{
|
||||
"grant_type": "refresh_token",
|
||||
"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})
|
||||
req := NewRequest(t, "GET", "/"+org22.Name)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/"+org22.Name).SetHeader("Accept", "application/rss+xml"), http.StatusNotFound)
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
oldName := org22.Name
|
||||
@@ -106,6 +107,8 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
|
||||
req = NewRequest(t, "GET", "/"+org23.Name)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
strangerSession := loginUser(t, "user4")
|
||||
strangerSession.MakeRequest(t, NewRequest(t, "POST", "/"+org23.Name+"?action=follow"), http.StatusNotFound)
|
||||
|
||||
oldName = org23.Name
|
||||
newName = "org23_renamed"
|
||||
@@ -127,6 +130,8 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
|
||||
req = NewRequest(t, "GET", "/"+user31.Name)
|
||||
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
|
||||
newName = "user31_renamed"
|
||||
@@ -330,6 +335,10 @@ func testGetUserRss(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req = NewRequestf(t, "GET", "/non-existent-user.rss")
|
||||
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) {
|
||||
|
||||
@@ -58,6 +58,7 @@ function selectRange(range: string): Element | null {
|
||||
stopLineNum = tmp;
|
||||
range = `${stop}-${start}`;
|
||||
}
|
||||
if (startLineNum < 1) return null;
|
||||
|
||||
const first = elLineNums[startLineNum - 1] ?? null;
|
||||
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:
|
||||
// - 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() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
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)!;
|
||||
if (textTarget.nodeName === 'INPUT' || textTarget.nodeName === 'TEXTAREA') {
|
||||
text = (textTarget as HTMLInputElement | HTMLTextAreaElement).value;
|
||||
} else if (textTarget.nodeName === 'DIV') {
|
||||
text = textTarget.textContent;
|
||||
} else {
|
||||
throw new Error(`Unsupported element for clipboard target: ${textSelector}`);
|
||||
text = textTarget.textContent;
|
||||
}
|
||||
}
|
||||
// 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 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}/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);
|
||||
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', () => {
|
||||
|
||||
@@ -58,6 +58,7 @@ export function isGiteaError(filename: string, stack: string): boolean {
|
||||
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
|
||||
const assetBaseUrl = new URL(`${windowConfig()?.assetUrlPrefix}/`, window.location.origin).href;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user