mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 17:44:24 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			266 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			266 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package setting
 | |
| 
 | |
| import (
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| )
 | |
| 
 | |
| // ExternalMarkupRenderers represents the external markup renderers
 | |
| var (
 | |
| 	ExternalMarkupRenderers    []*MarkupRenderer
 | |
| 	ExternalSanitizerRules     []MarkupSanitizerRule
 | |
| 	MermaidMaxSourceCharacters int
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	RenderContentModeSanitized   = "sanitized"
 | |
| 	RenderContentModeNoSanitizer = "no-sanitizer"
 | |
| 	RenderContentModeIframe      = "iframe"
 | |
| )
 | |
| 
 | |
| type MarkdownRenderOptions struct {
 | |
| 	NewLineHardBreak  bool
 | |
| 	ShortIssuePattern bool // Actually it is a "markup" option because it is used in "post processor"
 | |
| }
 | |
| 
 | |
| type MarkdownMathCodeBlockOptions struct {
 | |
| 	ParseInlineDollar        bool
 | |
| 	ParseInlineParentheses   bool
 | |
| 	ParseBlockDollar         bool
 | |
| 	ParseBlockSquareBrackets bool
 | |
| }
 | |
| 
 | |
| // Markdown settings
 | |
| var Markdown = struct {
 | |
| 	RenderOptionsComment  MarkdownRenderOptions `ini:"-"`
 | |
| 	RenderOptionsWiki     MarkdownRenderOptions `ini:"-"`
 | |
| 	RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"`
 | |
| 
 | |
| 	CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor"
 | |
| 	FileExtensions   []string
 | |
| 
 | |
| 	EnableMath             bool
 | |
| 	MathCodeBlockDetection []string
 | |
| 	MathCodeBlockOptions   MarkdownMathCodeBlockOptions `ini:"-"`
 | |
| }{
 | |
| 	FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
 | |
| 	EnableMath:     true,
 | |
| }
 | |
| 
 | |
| // MarkupRenderer defines the external parser configured in ini
 | |
| type MarkupRenderer struct {
 | |
| 	Enabled              bool
 | |
| 	MarkupName           string
 | |
| 	Command              string
 | |
| 	FileExtensions       []string
 | |
| 	IsInputFile          bool
 | |
| 	NeedPostProcess      bool
 | |
| 	MarkupSanitizerRules []MarkupSanitizerRule
 | |
| 	RenderContentMode    string
 | |
| }
 | |
| 
 | |
| // MarkupSanitizerRule defines the policy for whitelisting attributes on
 | |
| // certain elements.
 | |
| type MarkupSanitizerRule struct {
 | |
| 	Element            string
 | |
| 	AllowAttr          string
 | |
| 	Regexp             string
 | |
| 	AllowDataURIImages bool
 | |
| }
 | |
| 
 | |
| func loadMarkupFrom(rootCfg ConfigProvider) {
 | |
| 	mustMapSetting(rootCfg, "markdown", &Markdown)
 | |
| 	const none = "none"
 | |
| 
 | |
| 	const renderOptionShortIssuePattern = "short-issue-pattern"
 | |
| 	const renderOptionNewLineHardBreak = "new-line-hard-break"
 | |
| 	cfgMarkdown := rootCfg.Section("markdown")
 | |
| 	parseMarkdownRenderOptions := func(key string, defaults []string) (ret MarkdownRenderOptions) {
 | |
| 		options := cfgMarkdown.Key(key).Strings(",")
 | |
| 		options = util.IfEmpty(options, defaults)
 | |
| 		for _, opt := range options {
 | |
| 			switch opt {
 | |
| 			case renderOptionShortIssuePattern:
 | |
| 				ret.ShortIssuePattern = true
 | |
| 			case renderOptionNewLineHardBreak:
 | |
| 				ret.NewLineHardBreak = true
 | |
| 			case none:
 | |
| 				ret = MarkdownRenderOptions{}
 | |
| 			case "":
 | |
| 			default:
 | |
| 				log.Error("Unknown markdown render option in %s: %s", key, opt)
 | |
| 			}
 | |
| 		}
 | |
| 		return ret
 | |
| 	}
 | |
| 	Markdown.RenderOptionsComment = parseMarkdownRenderOptions("RENDER_OPTIONS_COMMENT", []string{renderOptionShortIssuePattern, renderOptionNewLineHardBreak})
 | |
| 	Markdown.RenderOptionsWiki = parseMarkdownRenderOptions("RENDER_OPTIONS_WIKI", []string{renderOptionShortIssuePattern})
 | |
| 	Markdown.RenderOptionsRepoFile = parseMarkdownRenderOptions("RENDER_OPTIONS_REPO_FILE", nil)
 | |
| 
 | |
| 	const mathCodeInlineDollar = "inline-dollar"
 | |
| 	const mathCodeInlineParentheses = "inline-parentheses"
 | |
| 	const mathCodeBlockDollar = "block-dollar"
 | |
| 	const mathCodeBlockSquareBrackets = "block-square-brackets"
 | |
| 	Markdown.MathCodeBlockDetection = util.IfEmpty(Markdown.MathCodeBlockDetection, []string{mathCodeInlineDollar, mathCodeBlockDollar})
 | |
| 	Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
 | |
| 	for _, s := range Markdown.MathCodeBlockDetection {
 | |
| 		switch s {
 | |
| 		case mathCodeInlineDollar:
 | |
| 			Markdown.MathCodeBlockOptions.ParseInlineDollar = true
 | |
| 		case mathCodeInlineParentheses:
 | |
| 			Markdown.MathCodeBlockOptions.ParseInlineParentheses = true
 | |
| 		case mathCodeBlockDollar:
 | |
| 			Markdown.MathCodeBlockOptions.ParseBlockDollar = true
 | |
| 		case mathCodeBlockSquareBrackets:
 | |
| 			Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true
 | |
| 		case none:
 | |
| 			Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
 | |
| 		case "":
 | |
| 		default:
 | |
| 			log.Error("Unknown math code block detection option: %s", s)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(50000)
 | |
| 	ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
 | |
| 	ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
 | |
| 
 | |
| 	for _, sec := range rootCfg.Section("markup").ChildSections() {
 | |
| 		name := strings.TrimPrefix(sec.Name(), "markup.")
 | |
| 		if name == "" {
 | |
| 			log.Warn("name is empty, markup " + sec.Name() + "ignored")
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if name == "sanitizer" || strings.HasPrefix(name, "sanitizer.") {
 | |
| 			newMarkupSanitizer(name, sec)
 | |
| 		} else {
 | |
| 			newMarkupRenderer(name, sec)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func newMarkupSanitizer(name string, sec ConfigSection) {
 | |
| 	rule, ok := createMarkupSanitizerRule(name, sec)
 | |
| 	if ok {
 | |
| 		if strings.HasPrefix(name, "sanitizer.") {
 | |
| 			names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2)
 | |
| 			name = names[0]
 | |
| 		}
 | |
| 		for _, renderer := range ExternalMarkupRenderers {
 | |
| 			if name == renderer.MarkupName {
 | |
| 				renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule)
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 		ExternalSanitizerRules = append(ExternalSanitizerRules, rule)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) {
 | |
| 	var rule MarkupSanitizerRule
 | |
| 
 | |
| 	ok := false
 | |
| 	if sec.HasKey("ALLOW_DATA_URI_IMAGES") {
 | |
| 		rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false)
 | |
| 		ok = true
 | |
| 	}
 | |
| 
 | |
| 	if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") {
 | |
| 		rule.Element = sec.Key("ELEMENT").Value()
 | |
| 		rule.AllowAttr = sec.Key("ALLOW_ATTR").Value()
 | |
| 
 | |
| 		if rule.Element == "" || rule.AllowAttr == "" {
 | |
| 			log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name)
 | |
| 			return rule, false
 | |
| 		}
 | |
| 
 | |
| 		regexpStr := sec.Key("REGEXP").Value()
 | |
| 		if regexpStr != "" {
 | |
| 			hasPrefix := strings.HasPrefix(regexpStr, "^")
 | |
| 			hasSuffix := strings.HasSuffix(regexpStr, "$")
 | |
| 			if !hasPrefix || !hasSuffix {
 | |
| 				log.Error("In markup.%s: REGEXP must start with ^ and end with $ to be strict", name)
 | |
| 				// to avoid breaking existing user configurations and satisfy the strict requirement in addSanitizerRules
 | |
| 				if !hasPrefix {
 | |
| 					regexpStr = "^.*" + regexpStr
 | |
| 				}
 | |
| 				if !hasSuffix {
 | |
| 					regexpStr += ".*$"
 | |
| 				}
 | |
| 			}
 | |
| 			_, err := regexp.Compile(regexpStr)
 | |
| 			if err != nil {
 | |
| 				log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err)
 | |
| 				return rule, false
 | |
| 			}
 | |
| 			rule.Regexp = regexpStr
 | |
| 		}
 | |
| 
 | |
| 		ok = true
 | |
| 	}
 | |
| 
 | |
| 	if !ok {
 | |
| 		log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name)
 | |
| 		return rule, false
 | |
| 	}
 | |
| 
 | |
| 	return rule, true
 | |
| }
 | |
| 
 | |
| func newMarkupRenderer(name string, sec ConfigSection) {
 | |
| 	extensionReg := regexp.MustCompile(`\.\w`)
 | |
| 
 | |
| 	extensions := sec.Key("FILE_EXTENSIONS").Strings(",")
 | |
| 	exts := make([]string, 0, len(extensions))
 | |
| 	for _, extension := range extensions {
 | |
| 		if !extensionReg.MatchString(extension) {
 | |
| 			log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored")
 | |
| 		} else {
 | |
| 			exts = append(exts, extension)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(exts) == 0 {
 | |
| 		log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	command := sec.Key("RENDER_COMMAND").MustString("")
 | |
| 	if command == "" {
 | |
| 		log.Warn(" RENDER_COMMAND is empty, markup " + name + " ignored")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if sec.HasKey("DISABLE_SANITIZER") {
 | |
| 		log.Error("Deprecated setting `[markup.*]` `DISABLE_SANITIZER` present. This fallback will be removed in v1.18.0")
 | |
| 	}
 | |
| 
 | |
| 	renderContentMode := sec.Key("RENDER_CONTENT_MODE").MustString(RenderContentModeSanitized)
 | |
| 	if !sec.HasKey("RENDER_CONTENT_MODE") && sec.Key("DISABLE_SANITIZER").MustBool(false) {
 | |
| 		renderContentMode = RenderContentModeNoSanitizer // if only the legacy DISABLE_SANITIZER exists, use it
 | |
| 	}
 | |
| 	if renderContentMode != RenderContentModeSanitized &&
 | |
| 		renderContentMode != RenderContentModeNoSanitizer &&
 | |
| 		renderContentMode != RenderContentModeIframe {
 | |
| 		log.Error("invalid RENDER_CONTENT_MODE: %q, default to %q", renderContentMode, RenderContentModeSanitized)
 | |
| 		renderContentMode = RenderContentModeSanitized
 | |
| 	}
 | |
| 
 | |
| 	ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
 | |
| 		Enabled:           sec.Key("ENABLED").MustBool(false),
 | |
| 		MarkupName:        name,
 | |
| 		FileExtensions:    exts,
 | |
| 		Command:           command,
 | |
| 		IsInputFile:       sec.Key("IS_INPUT_FILE").MustBool(false),
 | |
| 		NeedPostProcess:   sec.Key("NEED_POSTPROCESS").MustBool(true),
 | |
| 		RenderContentMode: renderContentMode,
 | |
| 	})
 | |
| }
 |