gitea源码

markup.go 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package setting
  4. import (
  5. "regexp"
  6. "strings"
  7. "code.gitea.io/gitea/modules/log"
  8. "code.gitea.io/gitea/modules/util"
  9. )
  10. // ExternalMarkupRenderers represents the external markup renderers
  11. var (
  12. ExternalMarkupRenderers []*MarkupRenderer
  13. ExternalSanitizerRules []MarkupSanitizerRule
  14. MermaidMaxSourceCharacters int
  15. )
  16. const (
  17. RenderContentModeSanitized = "sanitized"
  18. RenderContentModeNoSanitizer = "no-sanitizer"
  19. RenderContentModeIframe = "iframe"
  20. )
  21. type MarkdownRenderOptions struct {
  22. NewLineHardBreak bool
  23. ShortIssuePattern bool // Actually it is a "markup" option because it is used in "post processor"
  24. }
  25. type MarkdownMathCodeBlockOptions struct {
  26. ParseInlineDollar bool
  27. ParseInlineParentheses bool
  28. ParseBlockDollar bool
  29. ParseBlockSquareBrackets bool
  30. }
  31. // Markdown settings
  32. var Markdown = struct {
  33. RenderOptionsComment MarkdownRenderOptions `ini:"-"`
  34. RenderOptionsWiki MarkdownRenderOptions `ini:"-"`
  35. RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"`
  36. CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor"
  37. FileExtensions []string
  38. EnableMath bool
  39. MathCodeBlockDetection []string
  40. MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"`
  41. }{
  42. FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
  43. EnableMath: true,
  44. }
  45. // MarkupRenderer defines the external parser configured in ini
  46. type MarkupRenderer struct {
  47. Enabled bool
  48. MarkupName string
  49. Command string
  50. FileExtensions []string
  51. IsInputFile bool
  52. NeedPostProcess bool
  53. MarkupSanitizerRules []MarkupSanitizerRule
  54. RenderContentMode string
  55. }
  56. // MarkupSanitizerRule defines the policy for whitelisting attributes on
  57. // certain elements.
  58. type MarkupSanitizerRule struct {
  59. Element string
  60. AllowAttr string
  61. Regexp string
  62. AllowDataURIImages bool
  63. }
  64. func loadMarkupFrom(rootCfg ConfigProvider) {
  65. mustMapSetting(rootCfg, "markdown", &Markdown)
  66. const none = "none"
  67. const renderOptionShortIssuePattern = "short-issue-pattern"
  68. const renderOptionNewLineHardBreak = "new-line-hard-break"
  69. cfgMarkdown := rootCfg.Section("markdown")
  70. parseMarkdownRenderOptions := func(key string, defaults []string) (ret MarkdownRenderOptions) {
  71. options := cfgMarkdown.Key(key).Strings(",")
  72. options = util.IfEmpty(options, defaults)
  73. for _, opt := range options {
  74. switch opt {
  75. case renderOptionShortIssuePattern:
  76. ret.ShortIssuePattern = true
  77. case renderOptionNewLineHardBreak:
  78. ret.NewLineHardBreak = true
  79. case none:
  80. ret = MarkdownRenderOptions{}
  81. case "":
  82. default:
  83. log.Error("Unknown markdown render option in %s: %s", key, opt)
  84. }
  85. }
  86. return ret
  87. }
  88. Markdown.RenderOptionsComment = parseMarkdownRenderOptions("RENDER_OPTIONS_COMMENT", []string{renderOptionShortIssuePattern, renderOptionNewLineHardBreak})
  89. Markdown.RenderOptionsWiki = parseMarkdownRenderOptions("RENDER_OPTIONS_WIKI", []string{renderOptionShortIssuePattern})
  90. Markdown.RenderOptionsRepoFile = parseMarkdownRenderOptions("RENDER_OPTIONS_REPO_FILE", nil)
  91. const mathCodeInlineDollar = "inline-dollar"
  92. const mathCodeInlineParentheses = "inline-parentheses"
  93. const mathCodeBlockDollar = "block-dollar"
  94. const mathCodeBlockSquareBrackets = "block-square-brackets"
  95. Markdown.MathCodeBlockDetection = util.IfEmpty(Markdown.MathCodeBlockDetection, []string{mathCodeInlineDollar, mathCodeBlockDollar})
  96. Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
  97. for _, s := range Markdown.MathCodeBlockDetection {
  98. switch s {
  99. case mathCodeInlineDollar:
  100. Markdown.MathCodeBlockOptions.ParseInlineDollar = true
  101. case mathCodeInlineParentheses:
  102. Markdown.MathCodeBlockOptions.ParseInlineParentheses = true
  103. case mathCodeBlockDollar:
  104. Markdown.MathCodeBlockOptions.ParseBlockDollar = true
  105. case mathCodeBlockSquareBrackets:
  106. Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true
  107. case none:
  108. Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
  109. case "":
  110. default:
  111. log.Error("Unknown math code block detection option: %s", s)
  112. }
  113. }
  114. MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(50000)
  115. ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
  116. ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
  117. for _, sec := range rootCfg.Section("markup").ChildSections() {
  118. name := strings.TrimPrefix(sec.Name(), "markup.")
  119. if name == "" {
  120. log.Warn("name is empty, markup " + sec.Name() + "ignored")
  121. continue
  122. }
  123. if name == "sanitizer" || strings.HasPrefix(name, "sanitizer.") {
  124. newMarkupSanitizer(name, sec)
  125. } else {
  126. newMarkupRenderer(name, sec)
  127. }
  128. }
  129. }
  130. func newMarkupSanitizer(name string, sec ConfigSection) {
  131. rule, ok := createMarkupSanitizerRule(name, sec)
  132. if ok {
  133. if after, found := strings.CutPrefix(name, "sanitizer."); found {
  134. names := strings.SplitN(after, ".", 2)
  135. name = names[0]
  136. }
  137. for _, renderer := range ExternalMarkupRenderers {
  138. if name == renderer.MarkupName {
  139. renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
  140. return
  141. }
  142. }
  143. ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
  144. }
  145. }
  146. func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) {
  147. var rule MarkupSanitizerRule
  148. ok := false
  149. if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
  150. rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
  151. ok = true
  152. }
  153. if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
  154. rule.Element = sec.Key("ELEMENT").Value()
  155. rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
  156. if rule.Element == "" || rule.AllowAttr == "" {
  157. log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
  158. return rule, false
  159. }
  160. regexpStr := sec.Key("REGEXP").Value()
  161. if regexpStr != "" {
  162. hasPrefix := strings.HasPrefix(regexpStr, "^")
  163. hasSuffix := strings.HasSuffix(regexpStr, "$")
  164. if !hasPrefix || !hasSuffix {
  165. log.Error("In markup.%s: REGEXP must start with ^ and end with $ to be strict", name)
  166. // to avoid breaking existing user configurations and satisfy the strict requirement in addSanitizerRules
  167. if !hasPrefix {
  168. regexpStr = "^.*" + regexpStr
  169. }
  170. if !hasSuffix {
  171. regexpStr += ".*$"
  172. }
  173. }
  174. _, err := regexp.Compile(regexpStr)
  175. if err != nil {
  176. log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
  177. return rule, false
  178. }
  179. rule.Regexp = regexpStr
  180. }
  181. ok = true
  182. }
  183. if !ok {
  184. log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
  185. return rule, false
  186. }
  187. return rule, true
  188. }
  189. func newMarkupRenderer(name string, sec ConfigSection) {
  190. extensionReg := regexp.MustCompile(`\.\w`)
  191. extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
  192. exts := make([]string, 0, len(extensions))
  193. for _, extension := range extensions {
  194. if !extensionReg.MatchString(extension) {
  195. log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
  196. } else {
  197. exts = append(exts, extension)
  198. }
  199. }
  200. if len(exts) == 0 {
  201. log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
  202. return
  203. }
  204. command := sec.Key("RENDER_COMMAND").MustString("")
  205. if command == "" {
  206. log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
  207. return
  208. }
  209. if sec.HasKey("DISABLE_SANITIZER") {
  210. log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
  211. }
  212. renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
  213. if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
  214. renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
  215. }
  216. if renderContentMode != RenderContentModeSanitized &&
  217. renderContentMode != RenderContentModeNoSanitizer &&
  218. renderContentMode != RenderContentModeIframe {
  219. log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
  220. renderContentMode = RenderContentModeSanitized
  221. }
  222. ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
  223. Enabled: sec.Key("ENABLED").MustBool(false),
  224. MarkupName: name,
  225. FileExtensions: exts,
  226. Command: command,
  227. IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
  228. NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
  229. RenderContentMode: renderContentMode,
  230. })
  231. }