0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-08 05:54:09 +02:00

Merge branch 'main' into webhook-bark

This commit is contained in:
ZeroDeng 2025-10-23 13:12:38 +08:00 committed by GitHub
commit d1bc442ebc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 339 additions and 223 deletions

View File

@ -2541,6 +2541,12 @@ LEVEL = Info
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code. ;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page. ;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
;RENDER_CONTENT_MODE=sanitized ;RENDER_CONTENT_MODE=sanitized
;;
;; Whether post-process the rendered HTML content, including:
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc.
;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false.
;NEED_POST_PROCESS=false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error {
return util.ErrNotExist return util.ErrNotExist
} }
// ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error.
type ErrLFSUnauthorizedAction struct {
RepoID int64
UserName string
Mode perm.AccessMode
}
// IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction.
func IsErrLFSUnauthorizedAction(err error) bool {
_, ok := err.(ErrLFSUnauthorizedAction)
return ok
}
func (err ErrLFSUnauthorizedAction) Error() string {
if err.Mode == perm.AccessModeWrite {
return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID)
}
return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID)
}
func (err ErrLFSUnauthorizedAction) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error. // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error.
type ErrLFSLockAlreadyExist struct { type ErrLFSLockAlreadyExist struct {
RepoID int64 RepoID int64
@ -93,12 +68,6 @@ type ErrLFSFileLocked struct {
UserName string UserName string
} }
// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked.
func IsErrLFSFileLocked(err error) bool {
_, ok := err.(ErrLFSFileLocked)
return ok
}
func (err ErrLFSFileLocked) Error() string { func (err ErrLFSFileLocked) Error() string {
return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path)
} }

View File

@ -11,10 +11,7 @@ import (
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error {
// CreateLFSLock creates a new lock. // CreateLFSLock creates a new lock.
func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil {
return nil, err
}
lock.Path = util.PathJoinRel(lock.Path) lock.Path = util.PathJoinRel(lock.Path)
lock.RepoID = repo.ID lock.RepoID = repo.ID
@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
return nil, err return nil, err
} }
if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil {
return nil, err
}
if !force && u.ID != lock.OwnerID { if !force && u.ID != lock.OwnerID {
return nil, errors.New("user doesn't own lock and force flag is not set") return nil, errors.New("user doesn't own lock and force flag is not set")
} }
@ -180,22 +169,3 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
return lock, nil return lock, nil
}) })
} }
// CheckLFSAccessForRepo check needed access mode base on action
func CheckLFSAccessForRepo(ctx context.Context, ownerID int64, repo *repo_model.Repository, mode perm.AccessMode) error {
if ownerID == 0 {
return ErrLFSUnauthorizedAction{repo.ID, "undefined", mode}
}
u, err := user_model.GetUserByID(ctx, ownerID)
if err != nil {
return err
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, u)
if err != nil {
return err
}
if !perm.CanAccess(mode, unit.TypeCode) {
return ErrLFSUnauthorizedAction{repo.ID, u.DisplayName(), mode}
}
return nil
}

View File

@ -5,9 +5,11 @@ package access
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"slices" "slices"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm" perm_model "code.gitea.io/gitea/models/perm"
@ -253,6 +255,34 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
} }
} }
// GetActionsUserRepoPermission returns the actions user permissions to the repository
func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) {
if actionsUser.ID != user_model.ActionsUserID {
return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user")
}
task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil {
return perm, err
}
if task.RepoID != repo.ID {
// FIXME allow public repo read access if tokenless pull is enabled
return perm, nil
}
var accessMode perm_model.AccessMode
if task.IsForkPullRequest {
accessMode = perm_model.AccessModeRead
} else {
accessMode = perm_model.AccessModeWrite
}
if err := repo.LoadUnits(ctx); err != nil {
return perm, err
}
perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode)
return perm, nil
}
// GetUserRepoPermission returns the user permissions to the repository // GetUserRepoPermission returns the user permissions to the repository
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
defer func() { defer func() {

View File

@ -249,8 +249,13 @@ func (u *User) MaxCreationLimit() int {
} }
// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised // NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user.
// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised.
// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future.
func (u *User) CanCreateRepoIn(owner *User) bool { func (u *User) CanCreateRepoIn(owner *User) bool {
if u.ID <= 0 || owner.ID <= 0 {
return false // fake user like Ghost or Actions user
}
if u.IsAdmin { if u.IsAdmin {
return true return true
} }

View File

@ -48,17 +48,16 @@ func IsGiteaActionsUserName(name string) bool {
// NewActionsUser creates and returns a fake user for running the actions. // NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User { func NewActionsUser() *User {
return &User{ return &User{
ID: ActionsUserID, ID: ActionsUserID,
Name: ActionsUserName, Name: ActionsUserName,
LowerName: ActionsUserName, LowerName: ActionsUserName,
IsActive: true, IsActive: true,
FullName: "Gitea Actions", FullName: "Gitea Actions",
Email: ActionsUserEmail, Email: ActionsUserEmail,
KeepEmailPrivate: true, KeepEmailPrivate: true,
LoginName: ActionsUserName, LoginName: ActionsUserName,
Type: UserTypeBot, Type: UserTypeBot,
AllowCreateOrganization: true, Visibility: structs.VisibleTypePublic,
Visibility: structs.VisibleTypePublic,
} }
} }

View File

@ -648,33 +648,36 @@ func TestGetInactiveUsers(t *testing.T) {
func TestCanCreateRepo(t *testing.T) { func TestCanCreateRepo(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
const noLimit = -1 const noLimit = -1
doerNormal := &user_model.User{} doerActions := user_model.NewActionsUser()
doerAdmin := &user_model.User{IsAdmin: true} doerNormal := &user_model.User{ID: 2}
doerAdmin := &user_model.User{ID: 1, IsAdmin: true}
t.Run("NoGlobalLimit", func(t *testing.T) { t.Run("NoGlobalLimit", func(t *testing.T) {
setting.Repository.MaxCreationLimit = noLimit setting.Repository.MaxCreationLimit = noLimit
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerAdmin.CanCreateRepoIn(doerActions))
}) })
t.Run("GlobalLimit50", func(t *testing.T) { t.Run("GlobalLimit50", func(t *testing.T) {
setting.Repository.MaxCreationLimit = 50 setting.Repository.MaxCreationLimit = 50
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
}) })
} }

View File

@ -15,6 +15,8 @@ import (
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/kballard/go-shellquote"
) )
// RegisterRenderers registers all supported third part renderers according settings // RegisterRenderers registers all supported third part renderers according settings
@ -81,7 +83,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
envMark("GITEA_PREFIX_SRC"), baseLinkSrc, envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
envMark("GITEA_PREFIX_RAW"), baseLinkRaw, envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
).Replace(p.Command) ).Replace(p.Command)
commands := strings.Fields(command) commands, err := shellquote.Split(command)
if err != nil || len(commands) == 0 {
return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err)
}
args := commands[1:] args := commands[1:]
if p.IsInputFile { if p.IsInputFile {

View File

@ -120,31 +120,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
return ctx return ctx
} }
// Render renders markup file to HTML with all specific handling stuff. // FindRendererByContext finds renderer by RenderContext
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { // TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
if ctx.RenderOptions.MarkupType == "" { if ctx.RenderOptions.MarkupType == "" {
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
} }
} }
renderer := renderers[ctx.RenderOptions.MarkupType] renderer := renderers[ctx.RenderOptions.MarkupType]
if renderer == nil { if renderer == nil {
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
} }
if ctx.RenderOptions.RelativePath != "" { return renderer, nil
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() { }
if !ctx.RenderOptions.InStandalonePage {
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
}
}
}
return render(ctx, renderer, input, output) func RendererNeedPostProcess(renderer Renderer) bool {
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
return true
}
return false
}
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
renderer, err := FindRendererByContext(ctx)
if err != nil {
return err
}
return RenderWithRenderer(ctx, renderer, input, output)
} }
// RenderString renders Markup string to HTML with all specific handling stuff and return string // RenderString renders Markup string to HTML with all specific handling stuff and return string
@ -185,7 +192,16 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
} }
} }
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
if !ctx.RenderOptions.InStandalonePage {
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
// otherwise, a <iframe> should be outputted to embed the external rendered page
return renderIFrame(ctx, output)
}
// else: this is a standalone page, fallthrough to the real rendering
}
ctx.usedByRender = true ctx.usedByRender = true
if ctx.RenderHelper != nil { if ctx.RenderHelper != nil {
defer ctx.RenderHelper.CleanUp() defer ctx.RenderHelper.CleanUp()
@ -214,7 +230,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
} }
eg.Go(func() (err error) { eg.Go(func() (err error) {
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() { if RendererNeedPostProcess(renderer) {
err = PostProcessDefault(ctx, pr1, pw2) err = PostProcessDefault(ctx, pr1, pw2)
} else { } else {
_, err = io.Copy(pw2, pr1) _, err = io.Copy(pw2, pr1)

View File

@ -259,7 +259,9 @@ func newMarkupRenderer(name string, sec ConfigSection) {
FileExtensions: exts, FileExtensions: exts,
Command: command, Command: command,
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
RenderContentMode: renderContentMode, RenderContentMode: renderContentMode,
// if no sanitizer is needed, no post process is needed
NeedPostProcess: sec.Key("NEED_POST_PROCESS").MustBool(renderContentMode == RenderContentModeSanitized),
}) })
} }

View File

@ -70,7 +70,6 @@ import (
"net/http" "net/http"
"strings" "strings"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -190,27 +189,11 @@ func repoAssignment() func(ctx *context.APIContext) {
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID {
taskID := ctx.Data["ActionsTaskID"].(int64) taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID) ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
} }
if task.RepoID != repo.ID {
ctx.APIErrorNotFound()
return
}
if task.IsForkPullRequest {
ctx.Repo.Permission.AccessMode = perm.AccessModeRead
} else {
ctx.Repo.Permission.AccessMode = perm.AccessModeWrite
}
if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
} else { } else {
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer) needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
if err != nil { if err != nil {

View File

@ -28,6 +28,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -270,6 +271,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
db.IsErrNamePatternNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) ||
label.IsErrTemplateLoad(err) { label.IsErrTemplateLoad(err) {
ctx.APIError(http.StatusUnprocessableEntity, err) ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
} else { } else {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
} }

View File

@ -18,7 +18,6 @@ import (
"sync" "sync"
"time" "time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -190,29 +189,17 @@ func httpBase(ctx *context.Context) *serviceHandler {
if ctx.Data["IsActionsToken"] == true { if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64) taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID) p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil { if err != nil {
ctx.ServerError("GetTaskByID", err) ctx.ServerError("GetUserRepoPermission", err)
return nil
}
if task.RepoID != repo.ID {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil return nil
} }
if task.IsForkPullRequest { if !p.CanAccess(accessMode, unitType) {
if accessMode > perm.AccessModeRead { ctx.PlainText(http.StatusNotFound, "Repository not found")
ctx.PlainText(http.StatusForbidden, "User permission denied") return nil
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
} else {
if accessMode > perm.AccessModeWrite {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
} }
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, p.UnitAccessMode(unitType)))
} else { } else {
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil { if err != nil {

View File

@ -151,17 +151,28 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
} }
func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) { func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) {
renderer, err := markup.FindRendererByContext(renderCtx)
if err != nil {
return nil, "", err
}
markupRd, markupWr := io.Pipe() markupRd, markupWr := io.Pipe()
defer markupWr.Close() defer markupWr.Close()
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
sb := &strings.Builder{} sb := &strings.Builder{}
// We allow NBSP here this is rendered if markup.RendererNeedPostProcess(renderer) {
escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP) // We allow NBSP here this is rendered
} else {
escaped = &charset.EscapeStatus{}
_, _ = io.Copy(sb, markupRd)
}
output = template.HTML(sb.String()) output = template.HTML(sb.String())
close(done) close(done)
}() }()
err = markup.Render(renderCtx, input, markupWr)
err = markup.RenderWithRenderer(renderCtx, renderer, input, markupWr)
_ = markupWr.CloseWithError(err) _ = markupWr.CloseWithError(err)
<-done <-done
return escaped, output, err return escaped, output, err

View File

@ -771,7 +771,7 @@ func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application {
// ToLFSLock convert a LFSLock to api.LFSLock // ToLFSLock convert a LFSLock to api.LFSLock
func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock { func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock {
u, err := user_model.GetUserByID(ctx, l.OwnerID) u, err := user_model.GetPossibleUserByID(ctx, l.OwnerID)
if err != nil { if err != nil {
return nil return nil
} }

View File

@ -187,13 +187,6 @@ func PostLockHandler(ctx *context.Context) {
}) })
return return
} }
if git_model.IsErrLFSUnauthorizedAction(err) {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
Message: "You must have push access to create locks : " + err.Error(),
})
return
}
log.Error("Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v", repository, req.Path, ctx.Doer, err) log.Error("Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v", repository, req.Path, ctx.Doer, err)
ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
Message: "internal server error : Internal Server Error", Message: "internal server error : Internal Server Error",
@ -317,13 +310,6 @@ func UnLockHandler(ctx *context.Context) {
lock, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), repository, ctx.Doer, req.Force) lock, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), repository, ctx.Doer, req.Force)
if err != nil { if err != nil {
if git_model.IsErrLFSUnauthorizedAction(err) {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="gitea-lfs"`)
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
Message: "You must have push access to delete locks : " + err.Error(),
})
return
}
log.Error("Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v", ctx.PathParamInt64("lid"), ctx.Doer, req.Force, err) log.Error("Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v", ctx.PathParamInt64("lid"), ctx.Doer, req.Force, err)
ctx.JSON(http.StatusInternalServerError, api.LFSLockError{ ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
Message: "unable to delete lock : Internal Server Error", Message: "unable to delete lock : Internal Server Error",

View File

@ -20,7 +20,6 @@ import (
"strings" "strings"
"time" "time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
perm_model "code.gitea.io/gitea/models/perm" perm_model "code.gitea.io/gitea/models/perm"
@ -549,33 +548,31 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
if ctx.Data["IsActionsToken"] == true { if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64) taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID) perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID)
if err != nil { if err != nil {
log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err) log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err)
return false return false
} }
if task.RepoID != repository.ID { return perm.CanAccess(accessMode, unit.TypeCode)
return false
}
if task.IsForkPullRequest {
return accessMode <= perm_model.AccessModeRead
}
return accessMode <= perm_model.AccessModeWrite
} }
// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess // it works for both anonymous request and signed-in user, then perm.CanAccess will do the permission check
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil { if err != nil {
log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err) log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository, err)
return false return false
} }
canRead := perm.CanAccess(accessMode, unit.TypeCode) canAccess := perm.CanAccess(accessMode, unit.TypeCode)
if canRead && (!requireSigned || ctx.IsSigned) { // if it doesn't require sign-in and anonymous user has access, return true
// if the user is already signed in (for example: by session auth method), and the doer can access, return true
if canAccess && (!requireSigned || ctx.IsSigned) {
return true return true
} }
// now, either sign-in is required or the ctx.Doer cannot access, check the LFS token
// however, "ctx.Doer exists but cannot access then check LFS token" should not really happen:
// * why a request can be sent with both valid user session and valid LFS token then use LFS token to access?
user, err := parseToken(ctx, authorization, repository, accessMode) user, err := parseToken(ctx, authorization, repository, accessMode)
if err != nil { if err != nil {
// Most of these are Warn level - the true internal server errors are logged in parseToken already // Most of these are Warn level - the true internal server errors are logged in parseToken already

View File

@ -0,0 +1,117 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"net/http"
"net/url"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsJobTokenAccess(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
t.Run("Write Access", testActionsJobTokenAccess(u, false))
t.Run("Read Access", testActionsJobTokenAccess(u, true))
})
}
func testActionsJobTokenAccess(u *url.URL, isFork bool) func(t *testing.T) {
return func(t *testing.T) {
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = isFork
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
require.NoError(t, err)
session := emptyTestSession(t)
context := APITestContext{
Session: session,
Token: task.Token,
Username: "user5",
Reponame: "repo4",
}
dstPath := t.TempDir()
u.Path = context.GitPath()
u.User = url.UserPassword("gitea-actions", task.Token)
t.Run("Git Clone", doGitClone(dstPath, u))
t.Run("API Get Repository", doAPIGetRepository(context, func(t *testing.T, r structs.Repository) {
require.Equal(t, "repo4", r.Name)
require.Equal(t, "user5", r.Owner.UserName)
}))
context.ExpectedCode = util.Iif(isFork, http.StatusForbidden, http.StatusCreated)
t.Run("API Create File", doAPICreateFile(context, "test.txt", &structs.CreateFileOptions{
FileOptions: structs.FileOptions{
NewBranchName: "new-branch",
Message: "Create File",
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`This is a test file created using job token.`)),
}))
context.ExpectedCode = http.StatusForbidden
t.Run("Fail to Create Repository", doAPICreateRepository(context, true))
context.ExpectedCode = http.StatusForbidden
t.Run("Fail to Delete Repository", doAPIDeleteRepository(context))
t.Run("Fail to Create Organization", doAPICreateOrganization(context, &structs.CreateOrgOption{
UserName: "actions",
FullName: "Gitea Actions",
}))
}
}
func TestActionsJobTokenAccessLFS(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
t.Run("Create Repository", doAPICreateRepository(httpContext, false, func(t *testing.T, repository structs.Repository) {
task := &actions_model.ActionTask{}
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = false
task.RepoID = repository.ID
err := db.Insert(t.Context(), task)
require.NoError(t, err)
session := emptyTestSession(t)
httpContext := APITestContext{
Session: session,
Token: task.Token,
Username: "user2",
Reponame: "repo-lfs-test",
}
u.Path = httpContext.GitPath()
dstPath := t.TempDir()
u.Path = httpContext.GitPath()
u.User = url.UserPassword("gitea-actions", task.Token)
t.Run("Clone", doGitClone(dstPath, u))
dstPath2 := t.TempDir()
t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(task.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length)
}))
})
}

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
// 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")
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository) httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository)
doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) { t.Run("repo-lfs-test", func(t *testing.T) {
u.Path = httpContext.GitPath() u.Path = httpContext.GitPath()
dstPath := t.TempDir() dstPath := t.TempDir()
@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0] lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs) reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK) respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length) assert.Equal(t, testFileSizeSmall, respLFS.Length)
}) })

View File

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAPILFSNotStarted(t *testing.T) { func TestAPILFSNotStarted(t *testing.T) {
@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) {
MakeRequest(t, req, http.StatusUnsupportedMediaType) MakeRequest(t, req, http.StatusUnsupportedMediaType)
} }
func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository { func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepo", doAPICreateRepository(ctx, false)) t.Run("CreateRepo", doAPICreateRepository(ctx, false))
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo") repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName)
assert.NoError(t, err) require.NoError(t, err)
return repo return repo
} }
@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) {
setting.LFS.StartServer = true setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "batch") repo := createLFSTestRepository(t, "lfs-batch-repo")
content := []byte("dummy1") content := []byte("dummy1")
oid := storeObjectInRepo(t, repo.ID, &content) oid := storeObjectInRepo(t, repo.ID, &content)
@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, exist) assert.True(t, exist)
repo2 := createLFSTestRepository(t, "batch2") repo2 := createLFSTestRepository(t, "lfs-batch2-repo")
content := []byte("dummy0") content := []byte("dummy0")
storeObjectInRepo(t, repo2.ID, &content) storeObjectInRepo(t, repo2.ID, &content)
@ -329,7 +330,7 @@ func TestAPILFSUpload(t *testing.T) {
setting.LFS.StartServer = true setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "upload") repo := createLFSTestRepository(t, "lfs-upload-repo")
content := []byte("dummy3") content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content) oid := storeObjectInRepo(t, repo.ID, &content)
@ -433,7 +434,7 @@ func TestAPILFSVerify(t *testing.T) {
setting.LFS.StartServer = true setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "verify") repo := createLFSTestRepository(t, "lfs-verify-repo")
content := []byte("dummy3") content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content) oid := storeObjectInRepo(t, repo.ID, &content)

View File

@ -4,18 +4,23 @@
package integration package integration
import ( import (
"bytes"
"io" "io"
"net/http" "net/http"
"net/url"
"strings" "strings"
"testing" "testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/markup/external"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestExternalMarkupRenderer(t *testing.T) { func TestExternalMarkupRenderer(t *testing.T) {
@ -25,36 +30,52 @@ func TestExternalMarkupRenderer(t *testing.T) {
return return
} }
req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") onGiteaRun(t, func(t *testing.T, _ *url.URL) {
resp := MakeRequest(t, req, http.StatusOK) t.Run("RenderNoSanitizer", func(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
_, err := createFile(user2, repo1, "file.no-sanitizer", "master", `any content`)
require.NoError(t, err)
bs, err := io.ReadAll(resp.Body) req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/file.no-sanitizer")
assert.NoError(t, err) resp := MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
div := doc.Find("div.file-view")
data, err := div.Html()
assert.NoError(t, err)
assert.Equal(t, `<script>window.alert("hi")</script>`, strings.TrimSpace(data))
})
})
doc := NewHTMLParser(t, bytes.NewBuffer(bs)) t.Run("RenderContentDirectly", func(t *testing.T) {
div := doc.Find("div.file-view") req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
data, err := div.Html() resp := MakeRequest(t, req, http.StatusOK)
assert.NoError(t, err) assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
r := markup.GetRendererByFileName("a.html").(*external.Renderer) doc := NewHTMLParser(t, resp.Body)
r.RenderContentMode = setting.RenderContentModeIframe div := doc.Find("div.file-view")
data, err := div.Html()
assert.NoError(t, err)
assert.Equal(t, "<div>\n\ttest external renderer\n</div>", strings.TrimSpace(data))
})
req = NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html") r := markup.GetRendererByFileName("any-file.html").(*external.Renderer)
resp = MakeRequest(t, req, http.StatusOK) defer test.MockVariableValue(&r.RenderContentMode, setting.RenderContentModeIframe)()
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
doc = NewHTMLParser(t, bytes.NewBuffer(bs))
iframe := doc.Find("iframe")
assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html") t.Run("RenderContentInIFrame", func(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK) req := NewRequest(t, "GET", "/user30/renderer/src/branch/master/README.html")
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type")) resp := MakeRequest(t, req, http.StatusOK)
bs, err = io.ReadAll(resp.Body) assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
assert.NoError(t, err) doc := NewHTMLParser(t, resp.Body)
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy")) iframe := doc.Find("iframe")
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs)) assert.Equal(t, "/user30/renderer/render/branch/master/README.html", iframe.AttrOr("src", ""))
req = NewRequest(t, "GET", "/user30/renderer/render/branch/master/README.html")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
bs, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "<div>\n\ttest external renderer\n</div>\n", string(bs))
})
} }

View File

@ -114,9 +114,16 @@ ENABLED = true
[markup.html] [markup.html]
ENABLED = true ENABLED = true
FILE_EXTENSIONS = .html FILE_EXTENSIONS = .html
RENDER_COMMAND = `go run build/test-echo.go` RENDER_COMMAND = go run build/test-echo.go
IS_INPUT_FILE = false ;RENDER_COMMAND = cat
RENDER_CONTENT_MODE=sanitized ;IS_INPUT_FILE = true
RENDER_CONTENT_MODE = sanitized
[markup.no-sanitizer]
ENABLED = true
FILE_EXTENSIONS = .no-sanitizer
RENDER_COMMAND = echo '<script>window.alert("hi")</script>'
RENDER_CONTENT_MODE = no-sanitizer
[actions] [actions]
ENABLED = true ENABLED = true

View File

@ -6,11 +6,9 @@ import {html, htmlRaw} from '../utils/html.ts';
const {mermaidMaxSourceCharacters} = window.config; const {mermaidMaxSourceCharacters} = window.config;
// margin removal is for https://github.com/mermaid-js/mermaid/issues/4907
const iframeCss = `:root {color-scheme: normal} const iframeCss = `:root {color-scheme: normal}
body {margin: 0; padding: 0; overflow: hidden} body {margin: 0; padding: 0; overflow: hidden}
#mermaid {display: block; margin: 0 auto} #mermaid {display: block; margin: 0 auto}`;
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> { export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
// .markup code.language-mermaid // .markup code.language-mermaid