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.9KB

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