diff options
author | Jonathan Tran <jonnytran@gmail.com> | 2021-01-12 22:45:19 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-12 22:45:19 -0500 |
commit | 81467e6f35f343b911c09f746deca869a48da4c8 (patch) | |
tree | 1b759007789bc0dbeeb543d54739ccf8b8dfb434 /modules/base | |
parent | 9465e60504284699078e620f7c892a9685d91458 (diff) | |
download | gitea-81467e6f35f343b911c09f746deca869a48da4c8.tar.gz gitea-81467e6f35f343b911c09f746deca869a48da4c8.zip |
Display SVG files as images instead of text (#14101)
* Change to display SVG files as images
* Remove unsafe styles from SVG CSP
* Add integration test to test SVG headers
* Add config setting to disable SVG rendering
* Add test for img tag when loading SVG image
* Remove the Raw view button for svg files since we don't fully support this
* Fix copyright year
* Rename and move config setting
* Add setting to cheat sheet in docs
* Fix so that comment matches cheat sheet
* Add allowing styles in CSP based on pull request feedback
* Re-enable raw button since we show SVG styles now
* Change so that SVG files are editable
* Add UI to toggle between source and rendered image for SVGs
* Change to show blame button for SVG images
* Fix to update ctx data
* Add test for DetectContentType when file is longer than sniffLen
Co-authored-by: Jonathan Tran <jon@allspice.io>
Co-authored-by: Kyle D <kdumontnu@gmail.com>
Diffstat (limited to 'modules/base')
-rw-r--r-- | modules/base/tool.go | 49 | ||||
-rw-r--r-- | modules/base/tool_test.go | 52 |
2 files changed, 96 insertions, 5 deletions
diff --git a/modules/base/tool.go b/modules/base/tool.go index 7ac572b85b..c497bee44a 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -15,6 +15,7 @@ import ( "net/http" "os" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -28,6 +29,15 @@ import ( "github.com/dustin/go-humanize" ) +// Use at most this many bytes to determine Content Type. +const sniffLen = 512 + +// SVGMimeType MIME type of SVG images. +const SVGMimeType = "image/svg+xml" + +var svgTagRegex = regexp.MustCompile(`(?s)\A\s*(?:<!--.*?-->\s*)*<svg\b`) +var svgTagInXMLRegex = regexp.MustCompile(`(?s)\A<\?xml\b.*?\?>\s*(?:<!--.*?-->\s*)*<svg\b`) + // EncodeMD5 encodes string to md5 hex value. func EncodeMD5(str string) string { m := md5.New() @@ -265,32 +275,61 @@ func IsLetter(ch rune) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch) } +// DetectContentType extends http.DetectContentType with more content types. +func DetectContentType(data []byte) string { + ct := http.DetectContentType(data) + + if len(data) > sniffLen { + data = data[:sniffLen] + } + + if setting.UI.SVG.Enabled && + ((strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")) && svgTagRegex.Match(data) || + strings.Contains(ct, "text/xml") && svgTagInXMLRegex.Match(data)) { + + // SVG is unsupported. https://github.com/golang/go/issues/15888 + return SVGMimeType + } + return ct +} + +// IsRepresentableAsText returns true if file content can be represented as +// plain text or is empty. +func IsRepresentableAsText(data []byte) bool { + return IsTextFile(data) || IsSVGImageFile(data) +} + // IsTextFile returns true if file content format is plain text or empty. func IsTextFile(data []byte) bool { if len(data) == 0 { return true } - return strings.Contains(http.DetectContentType(data), "text/") + return strings.Contains(DetectContentType(data), "text/") } // IsImageFile detects if data is an image format func IsImageFile(data []byte) bool { - return strings.Contains(http.DetectContentType(data), "image/") + return strings.Contains(DetectContentType(data), "image/") +} + +// IsSVGImageFile detects if data is an SVG image format +func IsSVGImageFile(data []byte) bool { + return strings.Contains(DetectContentType(data), SVGMimeType) } // IsPDFFile detects if data is a pdf format func IsPDFFile(data []byte) bool { - return strings.Contains(http.DetectContentType(data), "application/pdf") + return strings.Contains(DetectContentType(data), "application/pdf") } // IsVideoFile detects if data is an video format func IsVideoFile(data []byte) bool { - return strings.Contains(http.DetectContentType(data), "video/") + return strings.Contains(DetectContentType(data), "video/") } // IsAudioFile detects if data is an video format func IsAudioFile(data []byte) bool { - return strings.Contains(http.DetectContentType(data), "audio/") + return strings.Contains(DetectContentType(data), "audio/") } // EntryIcon returns the octicon class for displaying files/directories diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 0b708dafdb..cda1685da7 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -183,11 +183,63 @@ func TestIsLetter(t *testing.T) { assert.False(t, IsLetter('$')) } +func TestDetectContentTypeLongerThanSniffLen(t *testing.T) { + // Pre-condition: Shorter than sniffLen detects SVG. + assert.Equal(t, "image/svg+xml", DetectContentType([]byte(`<!-- Comment --><svg></svg>`))) + // Longer than sniffLen detects something else. + assert.Equal(t, "text/plain; charset=utf-8", DetectContentType([]byte(`<!-- +Comment Comment Comment Comment Comment Comment Comment Comment Comment Comment +Comment Comment Comment Comment Comment Comment Comment Comment Comment Comment +Comment Comment Comment Comment Comment Comment Comment Comment Comment Comment +Comment Comment Comment Comment Comment Comment Comment Comment Comment Comment +Comment Comment Comment Comment Comment Comment Comment Comment Comment Comment +Comment Comment Comment Comment Comment Comment Comment Comment Comment Comment +Comment Comment Comment --><svg></svg>`))) +} + func TestIsTextFile(t *testing.T) { assert.True(t, IsTextFile([]byte{})) assert.True(t, IsTextFile([]byte("lorem ipsum"))) } +func TestIsSVGImageFile(t *testing.T) { + assert.True(t, IsSVGImageFile([]byte("<svg></svg>"))) + assert.True(t, IsSVGImageFile([]byte(" <svg></svg>"))) + assert.True(t, IsSVGImageFile([]byte(`<svg width="100"></svg>`))) + assert.True(t, IsSVGImageFile([]byte("<svg/>"))) + assert.True(t, IsSVGImageFile([]byte(`<?xml version="1.0" encoding="UTF-8"?><svg></svg>`))) + assert.True(t, IsSVGImageFile([]byte(`<!-- Comment --> + <svg></svg>`))) + assert.True(t, IsSVGImageFile([]byte(`<!-- Multiple --> + <!-- Comments --> + <svg></svg>`))) + assert.True(t, IsSVGImageFile([]byte(`<!-- Multiline + Comment --> + <svg></svg>`))) + assert.True(t, IsSVGImageFile([]byte(`<?xml version="1.0" encoding="UTF-8"?> + <!-- Comment --> + <svg></svg>`))) + assert.True(t, IsSVGImageFile([]byte(`<?xml version="1.0" encoding="UTF-8"?> + <!-- Multiple --> + <!-- Comments --> + <svg></svg>`))) + assert.True(t, IsSVGImageFile([]byte(`<?xml version="1.0" encoding="UTF-8"?> + <!-- Multline + Comment --> + <svg></svg>`))) + assert.False(t, IsSVGImageFile([]byte{})) + assert.False(t, IsSVGImageFile([]byte("svg"))) + assert.False(t, IsSVGImageFile([]byte("<svgfoo></svgfoo>"))) + assert.False(t, IsSVGImageFile([]byte("text<svg></svg>"))) + assert.False(t, IsSVGImageFile([]byte("<html><body><svg></svg></body></html>"))) + assert.False(t, IsSVGImageFile([]byte(`<script>"<svg></svg>"</script>`))) + assert.False(t, IsSVGImageFile([]byte(`<!-- <svg></svg> inside comment --> + <foo></foo>`))) + assert.False(t, IsSVGImageFile([]byte(`<?xml version="1.0" encoding="UTF-8"?> + <!-- <svg></svg> inside comment --> + <foo></foo>`))) +} + func TestFormatNumberSI(t *testing.T) { assert.Equal(t, "125", FormatNumberSI(int(125))) assert.Equal(t, "1.3k", FormatNumberSI(int64(1317))) |