* 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>tags/v1.18.0-dev
@@ -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 |
@@ -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} |
@@ -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,13 +150,28 @@ 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, | |||
FileExtensions: exts, | |||
Command: command, | |||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), | |||
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true), | |||
DisableSanitizer: sec.Key("DISABLE_SANITIZER").MustBool(false), | |||
Enabled: sec.Key("ENABLED").MustBool(false), | |||
MarkupName: name, | |||
FileExtensions: exts, | |||
Command: command, | |||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), | |||
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true), | |||
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() | |||
} |
@@ -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 | |||
} | |||
} |
@@ -356,11 +356,11 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin | |||
ctx.Data["MarkupType"] = string(markupType) | |||
var result strings.Builder | |||
err := markup.Render(&markup.RenderContext{ | |||
Ctx: ctx, | |||
Filename: readmeFile.name, | |||
URLPrefix: readmeTreelink, | |||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), | |||
GitRepo: ctx.Repo.GitRepo, | |||
Ctx: ctx, | |||
RelativePath: ctx.Repo.TreePath, | |||
URLPrefix: readmeTreelink, | |||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), | |||
GitRepo: ctx.Repo.GitRepo, | |||
}, rd, &result) | |||
if err != nil { | |||
log.Error("Render failed: %v then fallback", err) | |||
@@ -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(), | |||
URLPrefix: path.Dir(treeLink), | |||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), | |||
GitRepo: ctx.Repo.GitRepo, | |||
Ctx: ctx, | |||
Type: markupType, | |||
RelativePath: ctx.Repo.TreePath, | |||
URLPrefix: path.Dir(treeLink), | |||
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{} | |||
@@ -627,11 +631,11 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | |||
ctx.Data["MarkupType"] = markupType | |||
var result strings.Builder | |||
err := markup.Render(&markup.RenderContext{ | |||
Ctx: ctx, | |||
Filename: blob.Name(), | |||
URLPrefix: path.Dir(treeLink), | |||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), | |||
GitRepo: ctx.Repo.GitRepo, | |||
Ctx: ctx, | |||
RelativePath: ctx.Repo.TreePath, | |||
URLPrefix: path.Dir(treeLink), | |||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(), | |||
GitRepo: ctx.Repo.GitRepo, | |||
}, rd, &result) | |||
if err != nil { | |||
ctx.ServerError("Render", err) |
@@ -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) |