You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

renderer.go 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "bytes"
  6. "context"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net/url"
  11. "path/filepath"
  12. "strings"
  13. "sync"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/setting"
  16. "github.com/yuin/goldmark/ast"
  17. )
  18. type RenderMetaMode string
  19. const (
  20. RenderMetaAsDetails RenderMetaMode = "details" // default
  21. RenderMetaAsNone RenderMetaMode = "none"
  22. RenderMetaAsTable RenderMetaMode = "table"
  23. )
  24. type ProcessorHelper struct {
  25. IsUsernameMentionable func(ctx context.Context, username string) bool
  26. ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
  27. }
  28. var DefaultProcessorHelper ProcessorHelper
  29. // Init initialize regexps for markdown parsing
  30. func Init(ph *ProcessorHelper) {
  31. if ph != nil {
  32. DefaultProcessorHelper = *ph
  33. }
  34. NewSanitizer()
  35. if len(setting.Markdown.CustomURLSchemes) > 0 {
  36. CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
  37. }
  38. // since setting maybe changed extensions, this will reload all renderer extensions mapping
  39. extRenderers = make(map[string]Renderer)
  40. for _, renderer := range renderers {
  41. for _, ext := range renderer.Extensions() {
  42. extRenderers[strings.ToLower(ext)] = renderer
  43. }
  44. }
  45. }
  46. // Header holds the data about a header.
  47. type Header struct {
  48. Level int
  49. Text string
  50. ID string
  51. }
  52. // RenderContext represents a render context
  53. type RenderContext struct {
  54. Ctx context.Context
  55. RelativePath string // relative path from tree root of the branch
  56. Type string
  57. IsWiki bool
  58. URLPrefix string
  59. Metas map[string]string
  60. DefaultLink string
  61. GitRepo *git.Repository
  62. ShaExistCache map[string]bool
  63. cancelFn func()
  64. SidebarTocNode ast.Node
  65. RenderMetaAs RenderMetaMode
  66. InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
  67. }
  68. // Cancel runs any cleanup functions that have been registered for this Ctx
  69. func (ctx *RenderContext) Cancel() {
  70. if ctx == nil {
  71. return
  72. }
  73. ctx.ShaExistCache = map[string]bool{}
  74. if ctx.cancelFn == nil {
  75. return
  76. }
  77. ctx.cancelFn()
  78. }
  79. // AddCancel adds the provided fn as a Cleanup for this Ctx
  80. func (ctx *RenderContext) AddCancel(fn func()) {
  81. if ctx == nil {
  82. return
  83. }
  84. oldCancelFn := ctx.cancelFn
  85. if oldCancelFn == nil {
  86. ctx.cancelFn = fn
  87. return
  88. }
  89. ctx.cancelFn = func() {
  90. defer oldCancelFn()
  91. fn()
  92. }
  93. }
  94. // Renderer defines an interface for rendering markup file to HTML
  95. type Renderer interface {
  96. Name() string // markup format name
  97. Extensions() []string
  98. SanitizerRules() []setting.MarkupSanitizerRule
  99. Render(ctx *RenderContext, input io.Reader, output io.Writer) error
  100. }
  101. // PostProcessRenderer defines an interface for renderers who need post process
  102. type PostProcessRenderer interface {
  103. NeedPostProcess() bool
  104. }
  105. // PostProcessRenderer defines an interface for external renderers
  106. type ExternalRenderer interface {
  107. // SanitizerDisabled disabled sanitize if return true
  108. SanitizerDisabled() bool
  109. // DisplayInIFrame represents whether render the content with an iframe
  110. DisplayInIFrame() bool
  111. }
  112. // RendererContentDetector detects if the content can be rendered
  113. // by specified renderer
  114. type RendererContentDetector interface {
  115. CanRender(filename string, input io.Reader) bool
  116. }
  117. var (
  118. extRenderers = make(map[string]Renderer)
  119. renderers = make(map[string]Renderer)
  120. )
  121. // RegisterRenderer registers a new markup file renderer
  122. func RegisterRenderer(renderer Renderer) {
  123. renderers[renderer.Name()] = renderer
  124. for _, ext := range renderer.Extensions() {
  125. extRenderers[strings.ToLower(ext)] = renderer
  126. }
  127. }
  128. // GetRendererByFileName get renderer by filename
  129. func GetRendererByFileName(filename string) Renderer {
  130. extension := strings.ToLower(filepath.Ext(filename))
  131. return extRenderers[extension]
  132. }
  133. // GetRendererByType returns a renderer according type
  134. func GetRendererByType(tp string) Renderer {
  135. return renderers[tp]
  136. }
  137. // DetectRendererType detects the markup type of the content
  138. func DetectRendererType(filename string, input io.Reader) string {
  139. buf, err := io.ReadAll(input)
  140. if err != nil {
  141. return ""
  142. }
  143. for _, renderer := range renderers {
  144. if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, bytes.NewReader(buf)) {
  145. return renderer.Name()
  146. }
  147. }
  148. return ""
  149. }
  150. // Render renders markup file to HTML with all specific handling stuff.
  151. func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
  152. if ctx.Type != "" {
  153. return renderByType(ctx, input, output)
  154. } else if ctx.RelativePath != "" {
  155. return renderFile(ctx, input, output)
  156. }
  157. return errors.New("Render options both filename and type missing")
  158. }
  159. // RenderString renders Markup string to HTML with all specific handling stuff and return string
  160. func RenderString(ctx *RenderContext, content string) (string, error) {
  161. var buf strings.Builder
  162. if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
  163. return "", err
  164. }
  165. return buf.String(), nil
  166. }
  167. type nopCloser struct {
  168. io.Writer
  169. }
  170. func (nopCloser) Close() error { return nil }
  171. func renderIFrame(ctx *RenderContext, output io.Writer) error {
  172. // set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
  173. // at the moment, only "allow-scripts" is allowed for sandbox mode.
  174. // "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
  175. // 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
  176. _, err := io.WriteString(output, fmt.Sprintf(`
  177. <iframe src="%s/%s/%s/render/%s/%s"
  178. name="giteaExternalRender"
  179. onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
  180. width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
  181. sandbox="allow-scripts"
  182. ></iframe>`,
  183. setting.AppSubURL,
  184. url.PathEscape(ctx.Metas["user"]),
  185. url.PathEscape(ctx.Metas["repo"]),
  186. ctx.Metas["BranchNameSubURL"],
  187. url.PathEscape(ctx.RelativePath),
  188. ))
  189. return err
  190. }
  191. func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
  192. var wg sync.WaitGroup
  193. var err error
  194. pr, pw := io.Pipe()
  195. defer func() {
  196. _ = pr.Close()
  197. _ = pw.Close()
  198. }()
  199. var pr2 io.ReadCloser
  200. var pw2 io.WriteCloser
  201. var sanitizerDisabled bool
  202. if r, ok := renderer.(ExternalRenderer); ok {
  203. sanitizerDisabled = r.SanitizerDisabled()
  204. }
  205. if !sanitizerDisabled {
  206. pr2, pw2 = io.Pipe()
  207. defer func() {
  208. _ = pr2.Close()
  209. _ = pw2.Close()
  210. }()
  211. wg.Add(1)
  212. go func() {
  213. err = SanitizeReader(pr2, renderer.Name(), output)
  214. _ = pr2.Close()
  215. wg.Done()
  216. }()
  217. } else {
  218. pw2 = nopCloser{output}
  219. }
  220. wg.Add(1)
  221. go func() {
  222. if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
  223. err = PostProcess(ctx, pr, pw2)
  224. } else {
  225. _, err = io.Copy(pw2, pr)
  226. }
  227. _ = pr.Close()
  228. _ = pw2.Close()
  229. wg.Done()
  230. }()
  231. if err1 := renderer.Render(ctx, input, pw); err1 != nil {
  232. return err1
  233. }
  234. _ = pw.Close()
  235. wg.Wait()
  236. return err
  237. }
  238. // ErrUnsupportedRenderType represents
  239. type ErrUnsupportedRenderType struct {
  240. Type string
  241. }
  242. func (err ErrUnsupportedRenderType) Error() string {
  243. return fmt.Sprintf("Unsupported render type: %s", err.Type)
  244. }
  245. func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error {
  246. if renderer, ok := renderers[ctx.Type]; ok {
  247. return render(ctx, renderer, input, output)
  248. }
  249. return ErrUnsupportedRenderType{ctx.Type}
  250. }
  251. // ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
  252. type ErrUnsupportedRenderExtension struct {
  253. Extension string
  254. }
  255. func IsErrUnsupportedRenderExtension(err error) bool {
  256. _, ok := err.(ErrUnsupportedRenderExtension)
  257. return ok
  258. }
  259. func (err ErrUnsupportedRenderExtension) Error() string {
  260. return fmt.Sprintf("Unsupported render extension: %s", err.Extension)
  261. }
  262. func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
  263. extension := strings.ToLower(filepath.Ext(ctx.RelativePath))
  264. if renderer, ok := extRenderers[extension]; ok {
  265. if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
  266. if !ctx.InStandalonePage {
  267. // for an external render, it could only output its content in a standalone page
  268. // otherwise, a <iframe> should be outputted to embed the external rendered page
  269. return renderIFrame(ctx, output)
  270. }
  271. }
  272. return render(ctx, renderer, input, output)
  273. }
  274. return ErrUnsupportedRenderExtension{extension}
  275. }
  276. // Type returns if markup format via the filename
  277. func Type(filename string) string {
  278. if parser := GetRendererByFileName(filename); parser != nil {
  279. return parser.Name()
  280. }
  281. return ""
  282. }
  283. // IsMarkupFile reports whether file is a markup type file
  284. func IsMarkupFile(name, markup string) bool {
  285. if parser := GetRendererByFileName(name); parser != nil {
  286. return parser.Name() == markup
  287. }
  288. return false
  289. }
  290. func PreviewableExtensions() []string {
  291. extensions := make([]string, 0, len(extRenderers))
  292. for extension := range extRenderers {
  293. extensions = append(extensions, extension)
  294. }
  295. return extensions
  296. }