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  }