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  }