mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +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:
		| @@ -2181,8 +2181,11 @@ PATH = | ||||
| ;RENDER_COMMAND = "asciidoc --out-file=- -" | ||||
| ;; Don't pass the file on STDIN, pass the filename as argument instead. | ||||
| ;IS_INPUT_FILE = false | ||||
| ; Don't filter html tags and attributes if true | ||||
| ;DISABLE_SANITIZER = false | ||||
| ;; 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. | ||||
| ;RENDER_CONTENT_MODE=sanitized | ||||
|  | ||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||
|   | ||||
| @@ -1026,13 +1026,16 @@ IS_INPUT_FILE = false | ||||
|    command. Multiple extensions needs a comma as splitter. | ||||
| - 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`. | ||||
| - 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: | ||||
| - `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. | ||||
|  | ||||
| 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 | ||||
| [markup.sanitizer.TeX] | ||||
|   | ||||
| @@ -318,14 +318,17 @@ IS_INPUT_FILE = false | ||||
| - FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。 | ||||
| - RENDER_COMMAND: 工具的命令行命令及参数。 | ||||
| - 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_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。 | ||||
|  | ||||
| 如果 `DISABLE_SANITIZER` 为 false,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。 | ||||
| 如果 `RENDER_CONTENT_MODE` 为 `sanitized`,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。 | ||||
|  | ||||
| ```ini | ||||
| [markup.sanitizer.TeX] | ||||
|   | ||||
| @@ -54,7 +54,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader) | ||||
| func determineDelimiter(ctx *markup.RenderContext, data []byte) rune { | ||||
| 	extension := ".csv" | ||||
| 	if ctx != nil { | ||||
| 		extension = strings.ToLower(filepath.Ext(ctx.Filename)) | ||||
| 		extension = strings.ToLower(filepath.Ext(ctx.RelativePath)) | ||||
| 	} | ||||
|  | ||||
| 	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 { | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -30,7 +30,7 @@ func TestRender_Commits(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Ctx:          git.DefaultContext, | ||||
| 			Filename:  ".md", | ||||
| 			RelativePath: ".md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 			Metas:        localMetas, | ||||
| 		}, input) | ||||
| @@ -80,7 +80,7 @@ func TestRender_CrossReferences(t *testing.T) { | ||||
|  | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    setting.AppSubURL, | ||||
| 			Metas:        localMetas, | ||||
| 		}, input) | ||||
| @@ -124,7 +124,7 @@ func TestRender_links(t *testing.T) { | ||||
|  | ||||
| 	test := func(input, expected string) { | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| @@ -223,7 +223,7 @@ func TestRender_email(t *testing.T) { | ||||
|  | ||||
| 	test := func(input, expected string) { | ||||
| 		res, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
| @@ -281,7 +281,7 @@ func TestRender_emoji(t *testing.T) { | ||||
| 	test := func(input, expected string) { | ||||
| 		expected = strings.ReplaceAll(expected, "&", "&") | ||||
| 		buffer, err := RenderString(&RenderContext{ | ||||
| 			Filename:  "a.md", | ||||
| 			RelativePath: "a.md", | ||||
| 			URLPrefix:    TestRepoURL, | ||||
| 		}, input) | ||||
| 		assert.NoError(t, err) | ||||
|   | ||||
| @@ -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" | ||||
| @@ -44,7 +45,7 @@ type Header struct { | ||||
| // RenderContext represents a render context | ||||
| type RenderContext struct { | ||||
| 	Ctx              context.Context | ||||
| 	Filename        string | ||||
| 	RelativePath     string // relative path from tree root of the branch | ||||
| 	Type             string | ||||
| 	IsWiki           bool | ||||
| 	URLPrefix        string | ||||
| @@ -54,6 +55,7 @@ type RenderContext struct { | ||||
| 	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} | ||||
|   | ||||
| @@ -20,6 +20,12 @@ var ( | ||||
| 	MermaidMaxSourceCharacters int | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	RenderContentModeSanitized   = "sanitized" | ||||
| 	RenderContentModeNoSanitizer = "no-sanitizer" | ||||
| 	RenderContentModeIframe      = "iframe" | ||||
| ) | ||||
|  | ||||
| // MarkupRenderer defines the external parser configured in ini | ||||
| type MarkupRenderer struct { | ||||
| 	Enabled              bool | ||||
| @@ -29,7 +35,7 @@ type MarkupRenderer struct { | ||||
| 	IsInputFile          bool | ||||
| 	NeedPostProcess      bool | ||||
| 	MarkupSanitizerRules []MarkupSanitizerRule | ||||
| 	DisableSanitizer     bool | ||||
| 	RenderContentMode    string | ||||
| } | ||||
|  | ||||
| // MarkupSanitizerRule defines the policy for whitelisting attributes on | ||||
| @@ -144,6 +150,21 @@ func newMarkupRenderer(name string, sec *ini.Section) { | ||||
| 		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{ | ||||
| 		Enabled:           sec.Key("ENABLED").MustBool(false), | ||||
| 		MarkupName:        name, | ||||
| @@ -151,6 +172,6 @@ func newMarkupRenderer(name string, sec *ini.Section) { | ||||
| 		Command:           command, | ||||
| 		IsInputFile:       sec.Key("IS_INPUT_FILE").MustBool(false), | ||||
| 		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 | ||||
| 		} | ||||
|  | ||||
| 		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 { | ||||
| 			defer baseBlobCloser.Close() | ||||
| 		} | ||||
| @@ -151,7 +151,7 @@ func setCsvCompareContext(ctx *context.Context) { | ||||
| 			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 { | ||||
| 			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 | ||||
| 		err := markup.Render(&markup.RenderContext{ | ||||
| 			Ctx:          ctx, | ||||
| 			Filename:  readmeFile.name, | ||||
| 			RelativePath: ctx.Repo.TreePath, | ||||
| 			URLPrefix:    readmeTreelink, | ||||
| 			Metas:        ctx.Repo.Repository.ComposeDocumentMetas(), | ||||
| 			GitRepo:      ctx.Repo.GitRepo, | ||||
| @@ -528,18 +528,22 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | ||||
| 			if !detected { | ||||
| 				markupType = "" | ||||
| 			} | ||||
| 			metas := ctx.Repo.Repository.ComposeDocumentMetas() | ||||
| 			metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() | ||||
| 			err := markup.Render(&markup.RenderContext{ | ||||
| 				Ctx:          ctx, | ||||
| 				Type:         markupType, | ||||
| 				Filename:  blob.Name(), | ||||
| 				RelativePath: ctx.Repo.TreePath, | ||||
| 				URLPrefix:    path.Dir(treeLink), | ||||
| 				Metas:     ctx.Repo.Repository.ComposeDocumentMetas(), | ||||
| 				Metas:        metas, | ||||
| 				GitRepo:      ctx.Repo.GitRepo, | ||||
| 			}, rd, &result) | ||||
| 			if err != nil { | ||||
| 				ctx.ServerError("Render", err) | ||||
| 				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()) | ||||
| 		} else if readmeExist && !shouldRenderSource { | ||||
| 			buf := &bytes.Buffer{} | ||||
| @@ -628,7 +632,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | ||||
| 			var result strings.Builder | ||||
| 			err := markup.Render(&markup.RenderContext{ | ||||
| 				Ctx:          ctx, | ||||
| 				Filename:  blob.Name(), | ||||
| 				RelativePath: ctx.Repo.TreePath, | ||||
| 				URLPrefix:    path.Dir(treeLink), | ||||
| 				Metas:        ctx.Repo.Repository.ComposeDocumentMetas(), | ||||
| 				GitRepo:      ctx.Repo.GitRepo, | ||||
|   | ||||
| @@ -1161,6 +1161,13 @@ func RegisterRoutes(m *web.Route) { | ||||
| 			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload) | ||||
| 		}, 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.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits) | ||||
| 			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user