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>
This commit is contained in:
Jonathan Tran 2021-01-12 22:45:19 -05:00 committed by GitHub
parent 9465e60504
commit 81467e6f35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 736 additions and 16 deletions

View file

@ -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

View file

@ -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)))

View file

@ -190,6 +190,10 @@ var (
EventSourceUpdateTime time.Duration
} `ini:"ui.notification"`
SVG struct {
Enabled bool `ini:"ENABLE_RENDER"`
} `ini:"ui.svg"`
Admin struct {
UserPagingNum int
RepoPagingNum int
@ -230,6 +234,11 @@ var (
MaxTimeout: 60 * time.Second,
EventSourceUpdateTime: 10 * time.Second,
},
SVG: struct {
Enabled bool `ini:"ENABLE_RENDER"`
}{
Enabled: true,
},
Admin: struct {
UserPagingNum int
RepoPagingNum int