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