forked from forgejo/forgejo
Allow render HTML with css/js external links (#19017)
* Allow render HTML with css/js external links * Fix bug because of filename escape chars * Fix lint * Update docs about new configuration item * Fix bug of render HTML in sub directory * Add CSP head for displaying iframe in rendering file * Fix test * Apply suggestions from code review Co-authored-by: delvh <dev.lh@web.de> * Some improvements * some improvement * revert change in SanitizerDisabled of external renderer * Add sandbox for iframe and support allow-scripts and allow-same-origin * refactor * fix * fix lint * fine tune * use single option RENDER_CONTENT_MODE, use sandbox=allow-scripts * fine tune CSP * Apply suggestions from code review Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
7d1770cd71
commit
b01dce2a6e
17 changed files with 248 additions and 93 deletions
|
@ -33,9 +33,6 @@ func (Renderer) Name() string {
|
|||
return MarkupName
|
||||
}
|
||||
|
||||
// NeedPostProcess implements markup.Renderer
|
||||
func (Renderer) NeedPostProcess() bool { return false }
|
||||
|
||||
// Extensions implements markup.Renderer
|
||||
func (Renderer) Extensions() []string {
|
||||
return []string{".sh-session"}
|
||||
|
@ -48,11 +45,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
}
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (Renderer) SanitizerDisabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CanRender implements markup.RendererContentDetector
|
||||
func (Renderer) CanRender(filename string, input io.Reader) bool {
|
||||
buf, err := io.ReadAll(input)
|
||||
|
|
|
@ -29,9 +29,6 @@ func (Renderer) Name() string {
|
|||
return "csv"
|
||||
}
|
||||
|
||||
// NeedPostProcess implements markup.Renderer
|
||||
func (Renderer) NeedPostProcess() bool { return false }
|
||||
|
||||
// Extensions implements markup.Renderer
|
||||
func (Renderer) Extensions() []string {
|
||||
return []string{".csv", ".tsv"}
|
||||
|
@ -46,11 +43,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
}
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (Renderer) SanitizerDisabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func writeField(w io.Writer, element, class, field string) error {
|
||||
if _, err := io.WriteString(w, "<"); err != nil {
|
||||
return err
|
||||
|
|
12
modules/markup/external/external.go
vendored
12
modules/markup/external/external.go
vendored
|
@ -34,6 +34,11 @@ type Renderer struct {
|
|||
*setting.MarkupRenderer
|
||||
}
|
||||
|
||||
var (
|
||||
_ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||
_ markup.ExternalRenderer = (*Renderer)(nil)
|
||||
)
|
||||
|
||||
// Name returns the external tool name
|
||||
func (p *Renderer) Name() string {
|
||||
return p.MarkupName
|
||||
|
@ -56,7 +61,12 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (p *Renderer) SanitizerDisabled() bool {
|
||||
return p.DisableSanitizer
|
||||
return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
|
||||
}
|
||||
|
||||
// DisplayInIFrame represents whether render the content with an iframe
|
||||
func (p *Renderer) DisplayInIFrame() bool {
|
||||
return p.RenderContentMode == setting.RenderContentModeIframe
|
||||
}
|
||||
|
||||
func envMark(envName string) string {
|
||||
|
|
|
@ -29,10 +29,10 @@ func TestRender_Commits(t *testing.T) {
|
|||
setting.AppURL = TestAppURL
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Filename: ".md",
|
||||
URLPrefix: TestRepoURL,
|
||||
Metas: localMetas,
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
URLPrefix: TestRepoURL,
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
|
@ -80,9 +80,9 @@ func TestRender_CrossReferences(t *testing.T) {
|
|||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&RenderContext{
|
||||
Filename: "a.md",
|
||||
URLPrefix: setting.AppSubURL,
|
||||
Metas: localMetas,
|
||||
RelativePath: "a.md",
|
||||
URLPrefix: setting.AppSubURL,
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
|
@ -124,8 +124,8 @@ func TestRender_links(t *testing.T) {
|
|||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&RenderContext{
|
||||
Filename: "a.md",
|
||||
URLPrefix: TestRepoURL,
|
||||
RelativePath: "a.md",
|
||||
URLPrefix: TestRepoURL,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
|
@ -223,8 +223,8 @@ func TestRender_email(t *testing.T) {
|
|||
|
||||
test := func(input, expected string) {
|
||||
res, err := RenderString(&RenderContext{
|
||||
Filename: "a.md",
|
||||
URLPrefix: TestRepoURL,
|
||||
RelativePath: "a.md",
|
||||
URLPrefix: TestRepoURL,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
|
||||
|
@ -281,8 +281,8 @@ func TestRender_emoji(t *testing.T) {
|
|||
test := func(input, expected string) {
|
||||
expected = strings.ReplaceAll(expected, "&", "&")
|
||||
buffer, err := RenderString(&RenderContext{
|
||||
Filename: "a.md",
|
||||
URLPrefix: TestRepoURL,
|
||||
RelativePath: "a.md",
|
||||
URLPrefix: TestRepoURL,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
|
|
|
@ -205,12 +205,14 @@ func init() {
|
|||
// Renderer implements markup.Renderer
|
||||
type Renderer struct{}
|
||||
|
||||
var _ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||
|
||||
// Name implements markup.Renderer
|
||||
func (Renderer) Name() string {
|
||||
return MarkupName
|
||||
}
|
||||
|
||||
// NeedPostProcess implements markup.Renderer
|
||||
// NeedPostProcess implements markup.PostProcessRenderer
|
||||
func (Renderer) NeedPostProcess() bool { return true }
|
||||
|
||||
// Extensions implements markup.Renderer
|
||||
|
@ -223,11 +225,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
return []setting.MarkupSanitizerRule{}
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (Renderer) SanitizerDisabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Render implements markup.Renderer
|
||||
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
return render(ctx, input, output)
|
||||
|
|
|
@ -29,12 +29,14 @@ func init() {
|
|||
// Renderer implements markup.Renderer for orgmode
|
||||
type Renderer struct{}
|
||||
|
||||
var _ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||
|
||||
// Name implements markup.Renderer
|
||||
func (Renderer) Name() string {
|
||||
return "orgmode"
|
||||
}
|
||||
|
||||
// NeedPostProcess implements markup.Renderer
|
||||
// NeedPostProcess implements markup.PostProcessRenderer
|
||||
func (Renderer) NeedPostProcess() bool { return true }
|
||||
|
||||
// Extensions implements markup.Renderer
|
||||
|
@ -47,11 +49,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
return []setting.MarkupSanitizerRule{}
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (Renderer) SanitizerDisabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Render renders orgmode rawbytes to HTML
|
||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
htmlWriter := org.NewHTMLWriter()
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -43,17 +44,18 @@ type Header struct {
|
|||
|
||||
// RenderContext represents a render context
|
||||
type RenderContext struct {
|
||||
Ctx context.Context
|
||||
Filename string
|
||||
Type string
|
||||
IsWiki bool
|
||||
URLPrefix string
|
||||
Metas map[string]string
|
||||
DefaultLink string
|
||||
GitRepo *git.Repository
|
||||
ShaExistCache map[string]bool
|
||||
cancelFn func()
|
||||
TableOfContents []Header
|
||||
Ctx context.Context
|
||||
RelativePath string // relative path from tree root of the branch
|
||||
Type string
|
||||
IsWiki bool
|
||||
URLPrefix string
|
||||
Metas map[string]string
|
||||
DefaultLink string
|
||||
GitRepo *git.Repository
|
||||
ShaExistCache map[string]bool
|
||||
cancelFn func()
|
||||
TableOfContents []Header
|
||||
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||
}
|
||||
|
||||
// Cancel runs any cleanup functions that have been registered for this Ctx
|
||||
|
@ -88,12 +90,24 @@ func (ctx *RenderContext) AddCancel(fn func()) {
|
|||
type Renderer interface {
|
||||
Name() string // markup format name
|
||||
Extensions() []string
|
||||
NeedPostProcess() bool
|
||||
SanitizerRules() []setting.MarkupSanitizerRule
|
||||
SanitizerDisabled() bool
|
||||
Render(ctx *RenderContext, input io.Reader, output io.Writer) error
|
||||
}
|
||||
|
||||
// PostProcessRenderer defines an interface for renderers who need post process
|
||||
type PostProcessRenderer interface {
|
||||
NeedPostProcess() bool
|
||||
}
|
||||
|
||||
// PostProcessRenderer defines an interface for external renderers
|
||||
type ExternalRenderer interface {
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
SanitizerDisabled() bool
|
||||
|
||||
// DisplayInIFrame represents whether render the content with an iframe
|
||||
DisplayInIFrame() bool
|
||||
}
|
||||
|
||||
// RendererContentDetector detects if the content can be rendered
|
||||
// by specified renderer
|
||||
type RendererContentDetector interface {
|
||||
|
@ -142,7 +156,7 @@ func DetectRendererType(filename string, input io.Reader) string {
|
|||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
if ctx.Type != "" {
|
||||
return renderByType(ctx, input, output)
|
||||
} else if ctx.Filename != "" {
|
||||
} else if ctx.RelativePath != "" {
|
||||
return renderFile(ctx, input, output)
|
||||
}
|
||||
return errors.New("Render options both filename and type missing")
|
||||
|
@ -163,6 +177,27 @@ type nopCloser struct {
|
|||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
func renderIFrame(ctx *RenderContext, output io.Writer) error {
|
||||
// set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
|
||||
// at the moment, only "allow-scripts" is allowed for sandbox mode.
|
||||
// "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token
|
||||
// TODO: when using dark theme, if the rendered content doesn't have proper style, the default text color is black, which is not easy to read
|
||||
_, err := io.WriteString(output, fmt.Sprintf(`
|
||||
<iframe src="%s/%s/%s/render/%s/%s"
|
||||
name="giteaExternalRender"
|
||||
onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
|
||||
width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
|
||||
sandbox="allow-scripts"
|
||||
></iframe>`,
|
||||
setting.AppSubURL,
|
||||
url.PathEscape(ctx.Metas["user"]),
|
||||
url.PathEscape(ctx.Metas["repo"]),
|
||||
ctx.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RelativePath),
|
||||
))
|
||||
return err
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
|
@ -175,7 +210,12 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
|||
var pr2 io.ReadCloser
|
||||
var pw2 io.WriteCloser
|
||||
|
||||
if !renderer.SanitizerDisabled() {
|
||||
var sanitizerDisabled bool
|
||||
if r, ok := renderer.(ExternalRenderer); ok {
|
||||
sanitizerDisabled = r.SanitizerDisabled()
|
||||
}
|
||||
|
||||
if !sanitizerDisabled {
|
||||
pr2, pw2 = io.Pipe()
|
||||
defer func() {
|
||||
_ = pr2.Close()
|
||||
|
@ -194,7 +234,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
|||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if renderer.NeedPostProcess() {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
err = PostProcess(ctx, pr, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr)
|
||||
|
@ -239,8 +279,15 @@ func (err ErrUnsupportedRenderExtension) Error() string {
|
|||
}
|
||||
|
||||
func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
extension := strings.ToLower(filepath.Ext(ctx.Filename))
|
||||
extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
|
||||
if renderer, ok := extRenderers[extension]; ok {
|
||||
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
|
||||
if !ctx.InStandalonePage {
|
||||
// for an external 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)
|
||||
}
|
||||
return ErrUnsupportedRenderExtension{extension}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue