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