code.gitea.io/gitea@v1.22.3/modules/markup/csv/csv.go (about)

     1  // Copyright 2018 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package markup
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"fmt"
    10  	"html"
    11  	"io"
    12  	"regexp"
    13  	"strconv"
    14  
    15  	"code.gitea.io/gitea/modules/csv"
    16  	"code.gitea.io/gitea/modules/markup"
    17  	"code.gitea.io/gitea/modules/setting"
    18  )
    19  
    20  func init() {
    21  	markup.RegisterRenderer(Renderer{})
    22  }
    23  
    24  // Renderer implements markup.Renderer for csv files
    25  type Renderer struct{}
    26  
    27  // Name implements markup.Renderer
    28  func (Renderer) Name() string {
    29  	return "csv"
    30  }
    31  
    32  // Extensions implements markup.Renderer
    33  func (Renderer) Extensions() []string {
    34  	return []string{".csv", ".tsv"}
    35  }
    36  
    37  // SanitizerRules implements markup.Renderer
    38  func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
    39  	return []setting.MarkupSanitizerRule{
    40  		{Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)},
    41  		{Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
    42  		{Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
    43  	}
    44  }
    45  
    46  func writeField(w io.Writer, element, class, field string) error {
    47  	if _, err := io.WriteString(w, "<"); err != nil {
    48  		return err
    49  	}
    50  	if _, err := io.WriteString(w, element); err != nil {
    51  		return err
    52  	}
    53  	if len(class) > 0 {
    54  		if _, err := io.WriteString(w, " class=\""); err != nil {
    55  			return err
    56  		}
    57  		if _, err := io.WriteString(w, class); err != nil {
    58  			return err
    59  		}
    60  		if _, err := io.WriteString(w, "\""); err != nil {
    61  			return err
    62  		}
    63  	}
    64  	if _, err := io.WriteString(w, ">"); err != nil {
    65  		return err
    66  	}
    67  	if _, err := io.WriteString(w, html.EscapeString(field)); err != nil {
    68  		return err
    69  	}
    70  	if _, err := io.WriteString(w, "</"); err != nil {
    71  		return err
    72  	}
    73  	if _, err := io.WriteString(w, element); err != nil {
    74  		return err
    75  	}
    76  	_, err := io.WriteString(w, ">")
    77  	return err
    78  }
    79  
    80  // Render implements markup.Renderer
    81  func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
    82  	tmpBlock := bufio.NewWriter(output)
    83  	maxSize := setting.UI.CSV.MaxFileSize
    84  
    85  	if maxSize == 0 {
    86  		return r.tableRender(ctx, input, tmpBlock)
    87  	}
    88  
    89  	rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	if int64(len(rawBytes)) <= maxSize {
    95  		return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
    96  	}
    97  	return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
    98  }
    99  
   100  func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
   101  	_, err := tmpBlock.WriteString("<pre>")
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	scan := bufio.NewScanner(input)
   107  	scan.Split(bufio.ScanRunes)
   108  	for scan.Scan() {
   109  		switch scan.Text() {
   110  		case `&`:
   111  			_, err = tmpBlock.WriteString("&amp;")
   112  		case `'`:
   113  			_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
   114  		case `<`:
   115  			_, err = tmpBlock.WriteString("&lt;")
   116  		case `>`:
   117  			_, err = tmpBlock.WriteString("&gt;")
   118  		case `"`:
   119  			_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
   120  		default:
   121  			_, err = tmpBlock.Write(scan.Bytes())
   122  		}
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  	if err = scan.Err(); err != nil {
   128  		return fmt.Errorf("fallbackRender scan: %w", err)
   129  	}
   130  
   131  	_, err = tmpBlock.WriteString("</pre>")
   132  	if err != nil {
   133  		return err
   134  	}
   135  	return tmpBlock.Flush()
   136  }
   137  
   138  func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
   139  	rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
   145  		return err
   146  	}
   147  	row := 1
   148  	for {
   149  		fields, err := rd.Read()
   150  		if err == io.EOF {
   151  			break
   152  		}
   153  		if err != nil {
   154  			continue
   155  		}
   156  		if _, err := tmpBlock.WriteString("<tr>"); err != nil {
   157  			return err
   158  		}
   159  		element := "td"
   160  		if row == 1 {
   161  			element = "th"
   162  		}
   163  		if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil {
   164  			return err
   165  		}
   166  		for _, field := range fields {
   167  			if err := writeField(tmpBlock, element, "", field); err != nil {
   168  				return err
   169  			}
   170  		}
   171  		if _, err := tmpBlock.WriteString("</tr>"); err != nil {
   172  			return err
   173  		}
   174  
   175  		row++
   176  	}
   177  	if _, err = tmpBlock.WriteString("</table>"); err != nil {
   178  		return err
   179  	}
   180  	return tmpBlock.Flush()
   181  }