gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "context"
  6. "fmt"
  7. "io"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/modules/markup/internal"
  13. "code.gitea.io/gitea/modules/setting"
  14. "code.gitea.io/gitea/modules/util"
  15. "github.com/yuin/goldmark/ast"
  16. "golang.org/x/sync/errgroup"
  17. )
  18. type RenderMetaMode string
  19. const (
  20. RenderMetaAsDetails RenderMetaMode = "details" // default
  21. RenderMetaAsNone RenderMetaMode = "none"
  22. RenderMetaAsTable RenderMetaMode = "table"
  23. )
  24. var RenderBehaviorForTesting struct {
  25. // Gitea will emit some additional attributes for various purposes, these attributes don't affect rendering.
  26. // But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes.
  27. DisableAdditionalAttributes bool
  28. }
  29. type RenderOptions struct {
  30. UseAbsoluteLink bool
  31. // relative path from tree root of the branch
  32. RelativePath string
  33. // eg: "orgmode", "asciicast", "console"
  34. // for file mode, it could be left as empty, and will be detected by file extension in RelativePath
  35. MarkupType string
  36. // user&repo, format&style&regexp (for external issue pattern), teams&org (for mention)
  37. // RefTypeNameSubURL (for iframe&asciicast)
  38. // markupAllowShortIssuePattern
  39. // markdownNewLineHardBreak
  40. Metas map[string]string
  41. // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
  42. InStandalonePage bool
  43. }
  44. // RenderContext represents a render context
  45. type RenderContext struct {
  46. ctx context.Context
  47. // the context might be used by the "render" function, but it might also be used by "postProcess" function
  48. usedByRender bool
  49. SidebarTocNode ast.Node
  50. RenderHelper RenderHelper
  51. RenderOptions RenderOptions
  52. RenderInternal internal.RenderInternal
  53. }
  54. func (ctx *RenderContext) Deadline() (deadline time.Time, ok bool) {
  55. return ctx.ctx.Deadline()
  56. }
  57. func (ctx *RenderContext) Done() <-chan struct{} {
  58. return ctx.ctx.Done()
  59. }
  60. func (ctx *RenderContext) Err() error {
  61. return ctx.ctx.Err()
  62. }
  63. func (ctx *RenderContext) Value(key any) any {
  64. return ctx.ctx.Value(key)
  65. }
  66. var _ context.Context = (*RenderContext)(nil)
  67. func NewRenderContext(ctx context.Context) *RenderContext {
  68. return &RenderContext{ctx: ctx, RenderHelper: &SimpleRenderHelper{}}
  69. }
  70. func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext {
  71. ctx.RenderOptions.MarkupType = typ
  72. return ctx
  73. }
  74. func (ctx *RenderContext) WithRelativePath(path string) *RenderContext {
  75. ctx.RenderOptions.RelativePath = path
  76. return ctx
  77. }
  78. func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext {
  79. ctx.RenderOptions.Metas = metas
  80. return ctx
  81. }
  82. func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext {
  83. ctx.RenderOptions.InStandalonePage = v
  84. return ctx
  85. }
  86. func (ctx *RenderContext) WithUseAbsoluteLink(v bool) *RenderContext {
  87. ctx.RenderOptions.UseAbsoluteLink = v
  88. return ctx
  89. }
  90. func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
  91. ctx.RenderHelper = helper
  92. return ctx
  93. }
  94. // Render renders markup file to HTML with all specific handling stuff.
  95. func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
  96. if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
  97. ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
  98. if ctx.RenderOptions.MarkupType == "" {
  99. return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
  100. }
  101. }
  102. renderer := renderers[ctx.RenderOptions.MarkupType]
  103. if renderer == nil {
  104. return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
  105. }
  106. if ctx.RenderOptions.RelativePath != "" {
  107. if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
  108. if !ctx.RenderOptions.InStandalonePage {
  109. // for an external "DisplayInIFrame" render, it could only output its content in a standalone page
  110. // otherwise, a <iframe> should be outputted to embed the external rendered page
  111. return renderIFrame(ctx, output)
  112. }
  113. }
  114. }
  115. return render(ctx, renderer, input, output)
  116. }
  117. // RenderString renders Markup string to HTML with all specific handling stuff and return string
  118. func RenderString(ctx *RenderContext, content string) (string, error) {
  119. var buf strings.Builder
  120. if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
  121. return "", err
  122. }
  123. return buf.String(), nil
  124. }
  125. func renderIFrame(ctx *RenderContext, output io.Writer) error {
  126. // set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
  127. // at the moment, only "allow-scripts" is allowed for sandbox mode.
  128. // "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
  129. // 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
  130. _, err := io.WriteString(output, fmt.Sprintf(`
  131. <iframe src="%s/%s/%s/render/%s/%s"
  132. name="giteaExternalRender"
  133. onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
  134. width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
  135. sandbox="allow-scripts"
  136. ></iframe>`,
  137. setting.AppSubURL,
  138. url.PathEscape(ctx.RenderOptions.Metas["user"]),
  139. url.PathEscape(ctx.RenderOptions.Metas["repo"]),
  140. ctx.RenderOptions.Metas["RefTypeNameSubURL"],
  141. url.PathEscape(ctx.RenderOptions.RelativePath),
  142. ))
  143. return err
  144. }
  145. func pipes() (io.ReadCloser, io.WriteCloser, func()) {
  146. pr, pw := io.Pipe()
  147. return pr, pw, func() {
  148. _ = pr.Close()
  149. _ = pw.Close()
  150. }
  151. }
  152. func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
  153. ctx.usedByRender = true
  154. if ctx.RenderHelper != nil {
  155. defer ctx.RenderHelper.CleanUp()
  156. }
  157. finalProcessor := ctx.RenderInternal.Init(output)
  158. defer finalProcessor.Close()
  159. // input -> (pw1=pr1) -> renderer -> (pw2=pr2) -> SanitizeReader -> finalProcessor -> output
  160. // no sanitizer: input -> (pw1=pr1) -> renderer -> pw2(finalProcessor) -> output
  161. pr1, pw1, close1 := pipes()
  162. defer close1()
  163. eg, _ := errgroup.WithContext(ctx)
  164. var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
  165. if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
  166. var pr2 io.ReadCloser
  167. var close2 func()
  168. pr2, pw2, close2 = pipes()
  169. defer close2()
  170. eg.Go(func() error {
  171. defer pr2.Close()
  172. return SanitizeReader(pr2, renderer.Name(), finalProcessor)
  173. })
  174. }
  175. eg.Go(func() (err error) {
  176. if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
  177. err = PostProcessDefault(ctx, pr1, pw2)
  178. } else {
  179. _, err = io.Copy(pw2, pr1)
  180. }
  181. _, _ = pr1.Close(), pw2.Close()
  182. return err
  183. })
  184. if err := renderer.Render(ctx, input, pw1); err != nil {
  185. return err
  186. }
  187. _ = pw1.Close()
  188. return eg.Wait()
  189. }
  190. // Init initializes the render global variables
  191. func Init(renderHelpFuncs *RenderHelperFuncs) {
  192. DefaultRenderHelperFuncs = renderHelpFuncs
  193. if len(setting.Markdown.CustomURLSchemes) > 0 {
  194. CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
  195. }
  196. // since setting maybe changed extensions, this will reload all renderer extensions mapping
  197. extRenderers = make(map[string]Renderer)
  198. for _, renderer := range renderers {
  199. for _, ext := range renderer.Extensions() {
  200. extRenderers[strings.ToLower(ext)] = renderer
  201. }
  202. }
  203. }
  204. func ComposeSimpleDocumentMetas() map[string]string {
  205. // TODO: there is no separate config option for "simple document" rendering, so temporarily use the same config as "repo file"
  206. return map[string]string{"markdownNewLineHardBreak": strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)}
  207. }
  208. type TestRenderHelper struct {
  209. ctx *RenderContext
  210. BaseLink string
  211. }
  212. func (r *TestRenderHelper) CleanUp() {}
  213. func (r *TestRenderHelper) IsCommitIDExisting(commitID string) bool {
  214. return strings.HasPrefix(commitID, "65f1bf2") //|| strings.HasPrefix(commitID, "88fc37a")
  215. }
  216. func (r *TestRenderHelper) ResolveLink(link, preferLinkType string) string {
  217. linkType, link := ParseRenderedLink(link, preferLinkType)
  218. switch linkType {
  219. case LinkTypeRoot:
  220. return r.ctx.ResolveLinkRoot(link)
  221. default:
  222. return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
  223. }
  224. }
  225. var _ RenderHelper = (*TestRenderHelper)(nil)
  226. // NewTestRenderContext is a helper function to create a RenderContext for testing purpose
  227. // It accepts string (BaseLink), map[string]string (Metas)
  228. func NewTestRenderContext(baseLinkOrMetas ...any) *RenderContext {
  229. if !setting.IsInTesting {
  230. panic("NewTestRenderContext should only be used in testing")
  231. }
  232. helper := &TestRenderHelper{}
  233. ctx := NewRenderContext(context.Background()).WithHelper(helper)
  234. helper.ctx = ctx
  235. for _, v := range baseLinkOrMetas {
  236. switch v := v.(type) {
  237. case string:
  238. helper.BaseLink = v
  239. case map[string]string:
  240. ctx = ctx.WithMetas(v)
  241. default:
  242. panic(fmt.Sprintf("unknown type %T", v))
  243. }
  244. }
  245. return ctx
  246. }