mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-10 04:05:42 +01:00
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
|
@ -2181,8 +2181,11 @@ PATH =
|
||||||
;RENDER_COMMAND = "asciidoc --out-file=- -"
|
;RENDER_COMMAND = "asciidoc --out-file=- -"
|
||||||
;; Don't pass the file on STDIN, pass the filename as argument instead.
|
;; Don't pass the file on STDIN, pass the filename as argument instead.
|
||||||
;IS_INPUT_FILE = false
|
;IS_INPUT_FILE = false
|
||||||
; Don't filter html tags and attributes if true
|
;; How the content will be rendered.
|
||||||
;DISABLE_SANITIZER = false
|
;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
|
||||||
|
;; * 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.
|
||||||
|
;RENDER_CONTENT_MODE=sanitized
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -1026,13 +1026,16 @@ IS_INPUT_FILE = false
|
||||||
command. Multiple extensions needs a comma as splitter.
|
command. Multiple extensions needs a comma as splitter.
|
||||||
- RENDER\_COMMAND: External command to render all matching extensions.
|
- RENDER\_COMMAND: External command to render all matching extensions.
|
||||||
- IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`.
|
- IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`.
|
||||||
- DISABLE_SANITIZER: **false** Don't filter html tags and attributes if true. Don't change this to true except you know what that means.
|
- RENDER_CONTENT_MODE: **sanitized** How the content will be rendered.
|
||||||
|
- sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in `[markup.sanitizer.*]`.
|
||||||
|
- 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.
|
||||||
|
|
||||||
Two special environment variables are passed to the render command:
|
Two special environment variables are passed to the render command:
|
||||||
- `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links.
|
- `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links.
|
||||||
- `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths.
|
- `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths.
|
||||||
|
|
||||||
If `DISABLE_SANITIZER` is false, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
|
If `RENDER_CONTENT_MODE` is `sanitized`, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[markup.sanitizer.TeX]
|
[markup.sanitizer.TeX]
|
||||||
|
|
|
@ -318,14 +318,17 @@ IS_INPUT_FILE = false
|
||||||
- FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。
|
- FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。
|
||||||
- RENDER_COMMAND: 工具的命令行命令及参数。
|
- RENDER_COMMAND: 工具的命令行命令及参数。
|
||||||
- IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。
|
- IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。
|
||||||
- DISABLE_SANITIZER: **false** 如果为 true 则不过滤 HTML 标签和属性。除非你知道这意味着什么,否则不要设置为 true。
|
- RENDER_CONTENT_MODE: **sanitized** 内容如何被渲染。
|
||||||
|
- sanitized: 对内容进行净化并渲染到当前页面中,仅有一部分 HTML 标签和属性是被允许的。
|
||||||
|
- no-sanitizer: 禁用净化器,把内容渲染到当前页面中。此模式是**不安全**的,如果内容中含有恶意代码,可能会导致 XSS 攻击。
|
||||||
|
- iframe: 把内容渲染在一个独立的页面中并使用 iframe 嵌入到当前页面中。使用的 iframe 工作在沙箱模式并禁用了同源请求,JS 代码被安全的从父页面中隔离出去。
|
||||||
|
|
||||||
以下两个环境变量将会被传递给渲染命令:
|
以下两个环境变量将会被传递给渲染命令:
|
||||||
|
|
||||||
- `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀,可以被用于链接的前缀。
|
- `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀,可以被用于链接的前缀。
|
||||||
- `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。
|
- `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。
|
||||||
|
|
||||||
如果 `DISABLE_SANITIZER` 为 false,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
|
如果 `RENDER_CONTENT_MODE` 为 `sanitized`,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[markup.sanitizer.TeX]
|
[markup.sanitizer.TeX]
|
||||||
|
|
|
@ -54,7 +54,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader)
|
||||||
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
|
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
|
||||||
extension := ".csv"
|
extension := ".csv"
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
extension = strings.ToLower(filepath.Ext(ctx.Filename))
|
extension = strings.ToLower(filepath.Ext(ctx.RelativePath))
|
||||||
}
|
}
|
||||||
|
|
||||||
var delimiter rune
|
var delimiter rune
|
||||||
|
|
|
@ -230,7 +230,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimiters`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for n, c := range cases {
|
for n, c := range cases {
|
||||||
delimiter := determineDelimiter(&markup.RenderContext{Filename: c.filename}, []byte(decodeSlashes(t, c.csv)))
|
delimiter := determineDelimiter(&markup.RenderContext{RelativePath: c.filename}, []byte(decodeSlashes(t, c.csv)))
|
||||||
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
|
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,6 @@ func (Renderer) Name() string {
|
||||||
return MarkupName
|
return MarkupName
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedPostProcess implements markup.Renderer
|
|
||||||
func (Renderer) NeedPostProcess() bool { return false }
|
|
||||||
|
|
||||||
// Extensions implements markup.Renderer
|
// Extensions implements markup.Renderer
|
||||||
func (Renderer) Extensions() []string {
|
func (Renderer) Extensions() []string {
|
||||||
return []string{".sh-session"}
|
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
|
// CanRender implements markup.RendererContentDetector
|
||||||
func (Renderer) CanRender(filename string, input io.Reader) bool {
|
func (Renderer) CanRender(filename string, input io.Reader) bool {
|
||||||
buf, err := io.ReadAll(input)
|
buf, err := io.ReadAll(input)
|
||||||
|
|
|
@ -29,9 +29,6 @@ func (Renderer) Name() string {
|
||||||
return "csv"
|
return "csv"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedPostProcess implements markup.Renderer
|
|
||||||
func (Renderer) NeedPostProcess() bool { return false }
|
|
||||||
|
|
||||||
// Extensions implements markup.Renderer
|
// Extensions implements markup.Renderer
|
||||||
func (Renderer) Extensions() []string {
|
func (Renderer) Extensions() []string {
|
||||||
return []string{".csv", ".tsv"}
|
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 {
|
func writeField(w io.Writer, element, class, field string) error {
|
||||||
if _, err := io.WriteString(w, "<"); err != nil {
|
if _, err := io.WriteString(w, "<"); err != nil {
|
||||||
return err
|
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
|
*setting.MarkupRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||||
|
_ markup.ExternalRenderer = (*Renderer)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
// Name returns the external tool name
|
// Name returns the external tool name
|
||||||
func (p *Renderer) Name() string {
|
func (p *Renderer) Name() string {
|
||||||
return p.MarkupName
|
return p.MarkupName
|
||||||
|
@ -56,7 +61,12 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||||
|
|
||||||
// SanitizerDisabled disabled sanitize if return true
|
// SanitizerDisabled disabled sanitize if return true
|
||||||
func (p *Renderer) SanitizerDisabled() bool {
|
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 {
|
func envMark(envName string) string {
|
||||||
|
|
|
@ -30,7 +30,7 @@ func TestRender_Commits(t *testing.T) {
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
buffer, err := RenderString(&RenderContext{
|
buffer, err := RenderString(&RenderContext{
|
||||||
Ctx: git.DefaultContext,
|
Ctx: git.DefaultContext,
|
||||||
Filename: ".md",
|
RelativePath: ".md",
|
||||||
URLPrefix: TestRepoURL,
|
URLPrefix: TestRepoURL,
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, input)
|
}, input)
|
||||||
|
@ -80,7 +80,7 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||||
|
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
buffer, err := RenderString(&RenderContext{
|
buffer, err := RenderString(&RenderContext{
|
||||||
Filename: "a.md",
|
RelativePath: "a.md",
|
||||||
URLPrefix: setting.AppSubURL,
|
URLPrefix: setting.AppSubURL,
|
||||||
Metas: localMetas,
|
Metas: localMetas,
|
||||||
}, input)
|
}, input)
|
||||||
|
@ -124,7 +124,7 @@ func TestRender_links(t *testing.T) {
|
||||||
|
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
buffer, err := RenderString(&RenderContext{
|
buffer, err := RenderString(&RenderContext{
|
||||||
Filename: "a.md",
|
RelativePath: "a.md",
|
||||||
URLPrefix: TestRepoURL,
|
URLPrefix: TestRepoURL,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -223,7 +223,7 @@ func TestRender_email(t *testing.T) {
|
||||||
|
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
res, err := RenderString(&RenderContext{
|
res, err := RenderString(&RenderContext{
|
||||||
Filename: "a.md",
|
RelativePath: "a.md",
|
||||||
URLPrefix: TestRepoURL,
|
URLPrefix: TestRepoURL,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -281,7 +281,7 @@ func TestRender_emoji(t *testing.T) {
|
||||||
test := func(input, expected string) {
|
test := func(input, expected string) {
|
||||||
expected = strings.ReplaceAll(expected, "&", "&")
|
expected = strings.ReplaceAll(expected, "&", "&")
|
||||||
buffer, err := RenderString(&RenderContext{
|
buffer, err := RenderString(&RenderContext{
|
||||||
Filename: "a.md",
|
RelativePath: "a.md",
|
||||||
URLPrefix: TestRepoURL,
|
URLPrefix: TestRepoURL,
|
||||||
}, input)
|
}, input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -205,12 +205,14 @@ func init() {
|
||||||
// Renderer implements markup.Renderer
|
// Renderer implements markup.Renderer
|
||||||
type Renderer struct{}
|
type Renderer struct{}
|
||||||
|
|
||||||
|
var _ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||||
|
|
||||||
// Name implements markup.Renderer
|
// Name implements markup.Renderer
|
||||||
func (Renderer) Name() string {
|
func (Renderer) Name() string {
|
||||||
return MarkupName
|
return MarkupName
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedPostProcess implements markup.Renderer
|
// NeedPostProcess implements markup.PostProcessRenderer
|
||||||
func (Renderer) NeedPostProcess() bool { return true }
|
func (Renderer) NeedPostProcess() bool { return true }
|
||||||
|
|
||||||
// Extensions implements markup.Renderer
|
// Extensions implements markup.Renderer
|
||||||
|
@ -223,11 +225,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||||
return []setting.MarkupSanitizerRule{}
|
return []setting.MarkupSanitizerRule{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizerDisabled disabled sanitize if return true
|
|
||||||
func (Renderer) SanitizerDisabled() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render implements markup.Renderer
|
// Render implements markup.Renderer
|
||||||
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||||
return render(ctx, input, output)
|
return render(ctx, input, output)
|
||||||
|
|
|
@ -29,12 +29,14 @@ func init() {
|
||||||
// Renderer implements markup.Renderer for orgmode
|
// Renderer implements markup.Renderer for orgmode
|
||||||
type Renderer struct{}
|
type Renderer struct{}
|
||||||
|
|
||||||
|
var _ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||||
|
|
||||||
// Name implements markup.Renderer
|
// Name implements markup.Renderer
|
||||||
func (Renderer) Name() string {
|
func (Renderer) Name() string {
|
||||||
return "orgmode"
|
return "orgmode"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedPostProcess implements markup.Renderer
|
// NeedPostProcess implements markup.PostProcessRenderer
|
||||||
func (Renderer) NeedPostProcess() bool { return true }
|
func (Renderer) NeedPostProcess() bool { return true }
|
||||||
|
|
||||||
// Extensions implements markup.Renderer
|
// Extensions implements markup.Renderer
|
||||||
|
@ -47,11 +49,6 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||||
return []setting.MarkupSanitizerRule{}
|
return []setting.MarkupSanitizerRule{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizerDisabled disabled sanitize if return true
|
|
||||||
func (Renderer) SanitizerDisabled() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render renders orgmode rawbytes to HTML
|
// Render renders orgmode rawbytes to HTML
|
||||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||||
htmlWriter := org.NewHTMLWriter()
|
htmlWriter := org.NewHTMLWriter()
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -44,7 +45,7 @@ type Header struct {
|
||||||
// RenderContext represents a render context
|
// RenderContext represents a render context
|
||||||
type RenderContext struct {
|
type RenderContext struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Filename string
|
RelativePath string // relative path from tree root of the branch
|
||||||
Type string
|
Type string
|
||||||
IsWiki bool
|
IsWiki bool
|
||||||
URLPrefix string
|
URLPrefix string
|
||||||
|
@ -54,6 +55,7 @@ type RenderContext struct {
|
||||||
ShaExistCache map[string]bool
|
ShaExistCache map[string]bool
|
||||||
cancelFn func()
|
cancelFn func()
|
||||||
TableOfContents []Header
|
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
|
// 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 {
|
type Renderer interface {
|
||||||
Name() string // markup format name
|
Name() string // markup format name
|
||||||
Extensions() []string
|
Extensions() []string
|
||||||
NeedPostProcess() bool
|
|
||||||
SanitizerRules() []setting.MarkupSanitizerRule
|
SanitizerRules() []setting.MarkupSanitizerRule
|
||||||
SanitizerDisabled() bool
|
|
||||||
Render(ctx *RenderContext, input io.Reader, output io.Writer) error
|
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
|
// RendererContentDetector detects if the content can be rendered
|
||||||
// by specified renderer
|
// by specified renderer
|
||||||
type RendererContentDetector interface {
|
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 {
|
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||||
if ctx.Type != "" {
|
if ctx.Type != "" {
|
||||||
return renderByType(ctx, input, output)
|
return renderByType(ctx, input, output)
|
||||||
} else if ctx.Filename != "" {
|
} else if ctx.RelativePath != "" {
|
||||||
return renderFile(ctx, input, output)
|
return renderFile(ctx, input, output)
|
||||||
}
|
}
|
||||||
return errors.New("Render options both filename and type missing")
|
return errors.New("Render options both filename and type missing")
|
||||||
|
@ -163,6 +177,27 @@ type nopCloser struct {
|
||||||
|
|
||||||
func (nopCloser) Close() error { return nil }
|
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 {
|
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var err error
|
var err error
|
||||||
|
@ -175,7 +210,12 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||||
var pr2 io.ReadCloser
|
var pr2 io.ReadCloser
|
||||||
var pw2 io.WriteCloser
|
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()
|
pr2, pw2 = io.Pipe()
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = pr2.Close()
|
_ = pr2.Close()
|
||||||
|
@ -194,7 +234,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
if renderer.NeedPostProcess() {
|
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||||
err = PostProcess(ctx, pr, pw2)
|
err = PostProcess(ctx, pr, pw2)
|
||||||
} else {
|
} else {
|
||||||
_, err = io.Copy(pw2, pr)
|
_, 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 {
|
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 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 render(ctx, renderer, input, output)
|
||||||
}
|
}
|
||||||
return ErrUnsupportedRenderExtension{extension}
|
return ErrUnsupportedRenderExtension{extension}
|
||||||
|
|
|
@ -20,6 +20,12 @@ var (
|
||||||
MermaidMaxSourceCharacters int
|
MermaidMaxSourceCharacters int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RenderContentModeSanitized = "sanitized"
|
||||||
|
RenderContentModeNoSanitizer = "no-sanitizer"
|
||||||
|
RenderContentModeIframe = "iframe"
|
||||||
|
)
|
||||||
|
|
||||||
// MarkupRenderer defines the external parser configured in ini
|
// MarkupRenderer defines the external parser configured in ini
|
||||||
type MarkupRenderer struct {
|
type MarkupRenderer struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
@ -29,7 +35,7 @@ type MarkupRenderer struct {
|
||||||
IsInputFile bool
|
IsInputFile bool
|
||||||
NeedPostProcess bool
|
NeedPostProcess bool
|
||||||
MarkupSanitizerRules []MarkupSanitizerRule
|
MarkupSanitizerRules []MarkupSanitizerRule
|
||||||
DisableSanitizer bool
|
RenderContentMode string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkupSanitizerRule defines the policy for whitelisting attributes on
|
// MarkupSanitizerRule defines the policy for whitelisting attributes on
|
||||||
|
@ -144,6 +150,21 @@ func newMarkupRenderer(name string, sec *ini.Section) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sec.HasKey("DISABLE_SANITIZER") {
|
||||||
|
log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
|
||||||
|
if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
|
||||||
|
renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
|
||||||
|
}
|
||||||
|
if renderContentMode != RenderContentModeSanitized &&
|
||||||
|
renderContentMode != RenderContentModeNoSanitizer &&
|
||||||
|
renderContentMode != RenderContentModeIframe {
|
||||||
|
log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
|
||||||
|
renderContentMode = RenderContentModeSanitized
|
||||||
|
}
|
||||||
|
|
||||||
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
||||||
Enabled: sec.Key("ENABLED").MustBool(false),
|
Enabled: sec.Key("ENABLED").MustBool(false),
|
||||||
MarkupName: name,
|
MarkupName: name,
|
||||||
|
@ -151,6 +172,6 @@ func newMarkupRenderer(name string, sec *ini.Section) {
|
||||||
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),
|
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
|
||||||
DisableSanitizer: sec.Key("DISABLE_SANITIZER").MustBool(false),
|
RenderContentMode: renderContentMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,7 +139,7 @@ func setCsvCompareContext(ctx *context.Context) {
|
||||||
return csvReader, reader, err
|
return csvReader, reader, err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, Filename: diffFile.OldName}, baseCommit)
|
baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.OldName}, baseCommit)
|
||||||
if baseBlobCloser != nil {
|
if baseBlobCloser != nil {
|
||||||
defer baseBlobCloser.Close()
|
defer baseBlobCloser.Close()
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ func setCsvCompareContext(ctx *context.Context) {
|
||||||
return CsvDiffResult{nil, "unable to load file from base commit"}
|
return CsvDiffResult{nil, "unable to load file from base commit"}
|
||||||
}
|
}
|
||||||
|
|
||||||
headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, Filename: diffFile.Name}, headCommit)
|
headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.Name}, headCommit)
|
||||||
if headBlobCloser != nil {
|
if headBlobCloser != nil {
|
||||||
defer headBlobCloser.Close()
|
defer headBlobCloser.Close()
|
||||||
}
|
}
|
||||||
|
|
79
routers/web/repo/render.go
Normal file
79
routers/web/repo/render.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/charset"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderFile renders a file by repos path
|
||||||
|
func RenderFile(ctx *context.Context) {
|
||||||
|
blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
|
||||||
|
if err != nil {
|
||||||
|
if git.IsErrNotExist(err) {
|
||||||
|
ctx.NotFound("GetBlobByPath", err)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetBlobByPath", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataRc, err := blob.DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("DataAsync", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dataRc.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, _ := util.ReadAtMost(dataRc, buf)
|
||||||
|
buf = buf[:n]
|
||||||
|
|
||||||
|
st := typesniffer.DetectContentType(buf)
|
||||||
|
isTextFile := st.IsText()
|
||||||
|
|
||||||
|
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
|
||||||
|
|
||||||
|
if markupType := markup.Type(blob.Name()); markupType == "" {
|
||||||
|
if isTextFile {
|
||||||
|
_, err = io.Copy(ctx.Resp, rd)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Copy", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Error(http.StatusInternalServerError, "Unsupported file type render")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
||||||
|
if ctx.Repo.TreePath != "" {
|
||||||
|
treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
|
||||||
|
err = markup.Render(&markup.RenderContext{
|
||||||
|
Ctx: ctx,
|
||||||
|
RelativePath: ctx.Repo.TreePath,
|
||||||
|
URLPrefix: path.Dir(treeLink),
|
||||||
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
||||||
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
|
InStandalonePage: true,
|
||||||
|
}, rd, ctx.Resp)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("Render", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -357,7 +357,7 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
err := markup.Render(&markup.RenderContext{
|
err := markup.Render(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Filename: readmeFile.name,
|
RelativePath: ctx.Repo.TreePath,
|
||||||
URLPrefix: readmeTreelink,
|
URLPrefix: readmeTreelink,
|
||||||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
||||||
GitRepo: ctx.Repo.GitRepo,
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
|
@ -528,18 +528,22 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
if !detected {
|
if !detected {
|
||||||
markupType = ""
|
markupType = ""
|
||||||
}
|
}
|
||||||
|
metas := ctx.Repo.Repository.ComposeDocumentMetas()
|
||||||
|
metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
|
||||||
err := markup.Render(&markup.RenderContext{
|
err := markup.Render(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Type: markupType,
|
Type: markupType,
|
||||||
Filename: blob.Name(),
|
RelativePath: ctx.Repo.TreePath,
|
||||||
URLPrefix: path.Dir(treeLink),
|
URLPrefix: path.Dir(treeLink),
|
||||||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
Metas: metas,
|
||||||
GitRepo: ctx.Repo.GitRepo,
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
}, rd, &result)
|
}, rd, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// to prevent iframe load third-party url
|
||||||
|
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String())
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String())
|
||||||
} else if readmeExist && !shouldRenderSource {
|
} else if readmeExist && !shouldRenderSource {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
@ -628,7 +632,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
err := markup.Render(&markup.RenderContext{
|
err := markup.Render(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Filename: blob.Name(),
|
RelativePath: ctx.Repo.TreePath,
|
||||||
URLPrefix: path.Dir(treeLink),
|
URLPrefix: path.Dir(treeLink),
|
||||||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
||||||
GitRepo: ctx.Repo.GitRepo,
|
GitRepo: ctx.Repo.GitRepo,
|
||||||
|
|
|
@ -1161,6 +1161,13 @@ func RegisterRoutes(m *web.Route) {
|
||||||
m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
|
m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
|
||||||
}, repo.MustBeNotEmpty, reqRepoCodeReader)
|
}, repo.MustBeNotEmpty, reqRepoCodeReader)
|
||||||
|
|
||||||
|
m.Group("/render", func() {
|
||||||
|
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RenderFile)
|
||||||
|
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RenderFile)
|
||||||
|
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RenderFile)
|
||||||
|
m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.RenderFile)
|
||||||
|
}, repo.MustBeNotEmpty, reqRepoCodeReader)
|
||||||
|
|
||||||
m.Group("/commits", func() {
|
m.Group("/commits", func() {
|
||||||
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
|
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
|
||||||
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
|
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
|
||||||
|
|
Loading…
Reference in a new issue