Skip to content

Commit

Permalink
Fix CSV rendering (go-gitea#29663)
Browse files Browse the repository at this point in the history
Fixes go-gitea#29663
Previously, when a CSV file was larger than the limit, the render function lost its function to render the code. There were also multiple reads to the file, in order to determine its size and render or pre-render.
This solution implements a new config variable MAX_ROWS, which corresponds to the “Maximum allowed rows to render CSV files. (0 for no limit)” and rewrites the Render function for CSV files in markup module. Now the render function only reads the file once, having MAX_FILE_SIZE+1 as a reader limit and MAX_ROWS as a row limit. When the file is larger than MAX_FILE_SIZE or has more rows than MAX_ROWS, it only renders until the limit, and displays a user-friendly warning informing that the rendered data is not complete, in the user's language.
The warning: ![image](https://s3.amazonaws.com/i.snag.gy/ieROGx.jpg)
  • Loading branch information
HenriquerPimentel committed Apr 2, 2024
1 parent b482567 commit 0bbe632
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 66 deletions.
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,9 @@ LEVEL = Info
;;
;; Maximum allowed file size in bytes to render CSV files as table. (Set to 0 for no limit).
;MAX_FILE_SIZE = 524288
;;
;; Maximum allowed rows to render CSV files. (Set to 0 for no limit)
;MAX_ROWS = 2000

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
82 changes: 26 additions & 56 deletions modules/markup/csv/csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ package markup

import (
"bufio"
"bytes"
"fmt"
"html"
"io"
"regexp"
Expand All @@ -15,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
)

func init() {
Expand All @@ -40,6 +39,8 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
{Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)},
{Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
{Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)},
{Element: "div", AllowAttr: "class", Regexp: regexp.MustCompile(`ui top attached warning message`)},
{Element: "a", AllowAttr: "href", Regexp: regexp.MustCompile(`\?display=source`)},
}
}

Expand Down Expand Up @@ -80,79 +81,32 @@ func writeField(w io.Writer, element, class, field string) error {
// Render implements markup.Renderer
func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
tmpBlock := bufio.NewWriter(output)
warnBlock := bufio.NewWriter(tmpBlock)
maxSize := setting.UI.CSV.MaxFileSize
maxRows := setting.UI.CSV.MaxRows

if maxSize == 0 {
return r.tableRender(ctx, input, tmpBlock)
if maxSize != 0 {
input = io.LimitReader(input, maxSize+1)
}

rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
if err != nil {
return err
}

if int64(len(rawBytes)) <= maxSize {
return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
}
return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
}

func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
_, err := tmpBlock.WriteString("<pre>")
if err != nil {
return err
}

scan := bufio.NewScanner(input)
scan.Split(bufio.ScanRunes)
for scan.Scan() {
switch scan.Text() {
case `&`:
_, err = tmpBlock.WriteString("&amp;")
case `'`:
_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
case `<`:
_, err = tmpBlock.WriteString("&lt;")
case `>`:
_, err = tmpBlock.WriteString("&gt;")
case `"`:
_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
default:
_, err = tmpBlock.Write(scan.Bytes())
}
if err != nil {
return err
}
}
if err = scan.Err(); err != nil {
return fmt.Errorf("fallbackRender scan: %w", err)
}

_, err = tmpBlock.WriteString("</pre>")
if err != nil {
return err
}
return tmpBlock.Flush()
}

func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
if err != nil {
return err
}

if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
return err
}

row := 1
for {
fields, err := rd.Read()
if err == io.EOF {
if err == io.EOF || (row >= maxRows && maxRows != 0) {
break
}
if err != nil {
continue
}

if _, err := tmpBlock.WriteString("<tr>"); err != nil {
return err
}
Expand All @@ -174,6 +128,22 @@ func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock

row++
}

// Check if maxRows or maxSize is reached, and if true, warn.
if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
locale := ctx.Ctx.Value(translation.ContextKey).(translation.Locale)

// Construct the HTML string
warn := string(`<div class="ui top attached warning message" tabindex="0">` + locale.TrString("repo.file_too_large") + ` <b><a class="source" href="?display=source">` + locale.TrString("repo.file_view_source") + `</a></b></div>`)

// Write the HTML string to the output
if _, err := warnBlock.WriteString(warn); err != nil {
return err
}
if err = warnBlock.Flush(); err != nil {
return err
}
}
if _, err = tmpBlock.WriteString("</table>"); err != nil {
return err
}
Expand Down
10 changes: 0 additions & 10 deletions modules/markup/csv/csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
package markup

import (
"bufio"
"bytes"
"strings"
"testing"

Expand All @@ -31,12 +29,4 @@ func TestRenderCSV(t *testing.T) {
assert.NoError(t, err)
assert.EqualValues(t, v, buf.String())
}

t.Run("fallbackRender", func(t *testing.T) {
var buf bytes.Buffer
err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
assert.NoError(t, err)
want := "<pre>1,&lt;a&gt;\n2,&lt;b&gt;</pre>"
assert.Equal(t, want, buf.String())
})
}
3 changes: 3 additions & 0 deletions modules/setting/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var UI = struct {

CSV struct {
MaxFileSize int64
MaxRows int
} `ini:"ui.csv"`

Admin struct {
Expand Down Expand Up @@ -108,8 +109,10 @@ var UI = struct {
},
CSV: struct {
MaxFileSize int64
MaxRows int
}{
MaxFileSize: 524288,
MaxRows: 2000,
},
Admin: struct {
UserPagingNum int
Expand Down

0 comments on commit 0bbe632

Please sign in to comment.