Skip to content

Commit 738822f

Browse files
Fix CSV rendering (#29663)
Fixes #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)
1 parent 63c80ae commit 738822f

File tree

4 files changed

+32
-66
lines changed

4 files changed

+32
-66
lines changed

custom/conf/app.example.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,9 @@ LEVEL = Info
13331333
;;
13341334
;; Maximum allowed file size in bytes to render CSV files as table. (Set to 0 for no limit).
13351335
;MAX_FILE_SIZE = 524288
1336+
;;
1337+
;; Maximum allowed rows to render CSV files. (Set to 0 for no limit)
1338+
;MAX_ROWS = 2000
13361339

13371340
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
13381341
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

modules/markup/csv/csv.go

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ package markup
55

66
import (
77
"bufio"
8-
"bytes"
9-
"fmt"
108
"html"
119
"io"
1210
"regexp"
@@ -15,6 +13,7 @@ import (
1513
"code.gitea.io/gitea/modules/csv"
1614
"code.gitea.io/gitea/modules/markup"
1715
"code.gitea.io/gitea/modules/setting"
16+
"code.gitea.io/gitea/modules/translation"
1817
)
1918

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

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

85-
if maxSize == 0 {
86-
return r.tableRender(ctx, input, tmpBlock)
88+
if maxSize != 0 {
89+
input = io.LimitReader(input, maxSize+1)
8790
}
8891

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 {
13992
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
14093
if err != nil {
14194
return err
14295
}
143-
14496
if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
14597
return err
14698
}
99+
147100
row := 1
148101
for {
149102
fields, err := rd.Read()
150-
if err == io.EOF {
103+
if err == io.EOF || (row >= maxRows && maxRows != 0) {
151104
break
152105
}
153106
if err != nil {
154107
continue
155108
}
109+
156110
if _, err := tmpBlock.WriteString("<tr>"); err != nil {
157111
return err
158112
}
@@ -174,6 +128,22 @@ func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock
174128

175129
row++
176130
}
131+
132+
// Check if maxRows or maxSize is reached, and if true, warn.
133+
if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
134+
locale := ctx.Ctx.Value(translation.ContextKey).(translation.Locale)
135+
136+
// Construct the HTML string
137+
warn := `<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>`
138+
139+
// Write the HTML string to the output
140+
if _, err := warnBlock.WriteString(warn); err != nil {
141+
return err
142+
}
143+
if err = warnBlock.Flush(); err != nil {
144+
return err
145+
}
146+
}
177147
if _, err = tmpBlock.WriteString("</table>"); err != nil {
178148
return err
179149
}

modules/markup/csv/csv_test.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
package markup
55

66
import (
7-
"bufio"
8-
"bytes"
97
"strings"
108
"testing"
119

@@ -31,12 +29,4 @@ func TestRenderCSV(t *testing.T) {
3129
assert.NoError(t, err)
3230
assert.EqualValues(t, v, buf.String())
3331
}
34-
35-
t.Run("fallbackRender", func(t *testing.T) {
36-
var buf bytes.Buffer
37-
err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
38-
assert.NoError(t, err)
39-
want := "<pre>1,&lt;a&gt;\n2,&lt;b&gt;</pre>"
40-
assert.Equal(t, want, buf.String())
41-
})
4232
}

modules/setting/ui.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ var UI = struct {
5252

5353
CSV struct {
5454
MaxFileSize int64
55+
MaxRows int
5556
} `ini:"ui.csv"`
5657

5758
Admin struct {
@@ -108,8 +109,10 @@ var UI = struct {
108109
},
109110
CSV: struct {
110111
MaxFileSize int64
112+
MaxRows int
111113
}{
112114
MaxFileSize: 524288,
115+
MaxRows: 2000,
113116
},
114117
Admin: struct {
115118
UserPagingNum int

0 commit comments

Comments
 (0)