1// Copyright 2019 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package setting
5
6import (
7 "regexp"
8 "strings"
9
10 "forgejo.org/modules/log"
11)
12
13// ExternalMarkupRenderers represents the external markup renderers
14var (
15 ExternalMarkupRenderers []*MarkupRenderer
16 ExternalSanitizerRules []MarkupSanitizerRule
17 MermaidMaxSourceCharacters int
18 FilePreviewMaxLines int
19)
20
21const (
22 RenderContentModeSanitized = "sanitized"
23 RenderContentModeNoSanitizer = "no-sanitizer"
24 RenderContentModeIframe = "iframe"
25)
26
27// Markdown settings
28var Markdown = struct {
29 EnableHardLineBreakInComments bool
30 EnableHardLineBreakInDocuments bool
31 CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"`
32 FileExtensions []string
33 EnableMath bool
34}{
35 EnableHardLineBreakInComments: true,
36 EnableHardLineBreakInDocuments: false,
37 FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
38 EnableMath: true,
39}
40
41// MarkupRenderer defines the external parser configured in ini
42type MarkupRenderer struct {
43 Enabled bool
44 MarkupName string
45 Command string
46 FileExtensions []string
47 IsInputFile bool
48 NeedPostProcess bool
49 MarkupSanitizerRules []MarkupSanitizerRule
50 RenderContentMode string
51}
52
53// MarkupSanitizerRule defines the policy for whitelisting attributes on
54// certain elements.
55type MarkupSanitizerRule struct {
56 Element string
57 AllowAttr string
58 Regexp *regexp.Regexp
59 AllowDataURIImages bool
60}
61
62func loadMarkupFrom(rootCfg ConfigProvider) {
63 mustMapSetting(rootCfg, "markdown", &Markdown)
64
65 MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
66 FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50)
67 ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
68 ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
69
70 for _, sec := range rootCfg.Section("markup").ChildSections() {
71 name := strings.TrimPrefix(sec.Name(), "markup.")
72 if name == "" {
73 log.Warn("name is empty, markup " + sec.Name() + "ignored")
74 continue
75 }
76
77 if name == "sanitizer" || strings.HasPrefix(name, "sanitizer.") {
78 newMarkupSanitizer(name, sec)
79 } else {
80 newMarkupRenderer(name, sec)
81 }
82 }
83}
84
85func newMarkupSanitizer(name string, sec ConfigSection) {
86 rule, ok := createMarkupSanitizerRule(name, sec)
87 if ok {
88 if strings.HasPrefix(name, "sanitizer.") {
89 names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2)
90 name = names[0]
91 }
92 for _, renderer := range ExternalMarkupRenderers {
93 if name == renderer.MarkupName {
94 renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
95 return
96 }
97 }
98 ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
99 }
100}
101
102func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) {
103 var rule MarkupSanitizerRule
104
105 ok := false
106 if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
107 rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
108 ok = true
109 }
110
111 if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
112 rule.Element = sec.Key("ELEMENT").Value()
113 rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
114
115 if rule.Element == "" || rule.AllowAttr == "" {
116 log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
117 return rule, false
118 }
119
120 regexpStr := sec.Key("REGEXP").Value()
121 if regexpStr != "" {
122 // Validate when parsing the config that this is a valid regular
123 // expression. Then we can use regexp.MustCompile(...) later.
124 compiled, err := regexp.Compile(regexpStr)
125 if err != nil {
126 log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
127 return rule, false
128 }
129
130 rule.Regexp = compiled
131 }
132
133 ok = true
134 }
135
136 if !ok {
137 log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
138 return rule, false
139 }
140
141 return rule, true
142}
143
144func newMarkupRenderer(name string, sec ConfigSection) {
145 extensionReg := regexp.MustCompile(`\.\w`)
146
147 extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
148 exts := make([]string, 0, len(extensions))
149 for _, extension := range extensions {
150 if !extensionReg.MatchString(extension) {
151 log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
152 } else {
153 exts = append(exts, extension)
154 }
155 }
156
157 if len(exts) == 0 {
158 log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
159 return
160 }
161
162 command := sec.Key("RENDER_COMMAND").MustString("")
163 if command == "" {
164 log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
165 return
166 }
167
168 if sec.HasKey("DISABLE_SANITIZER") {
169 log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
170 }
171
172 renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
173 if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
174 renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
175 }
176 if renderContentMode != RenderContentModeSanitized &&
177 renderContentMode != RenderContentModeNoSanitizer &&
178 renderContentMode != RenderContentModeIframe {
179 log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
180 renderContentMode = RenderContentModeSanitized
181 }
182
183 ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
184 Enabled: sec.Key("ENABLED").MustBool(false),
185 MarkupName: name,
186 FileExtensions: exts,
187 Command: command,
188 IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
189 NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
190 RenderContentMode: renderContentMode,
191 })
192}