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:
parent
9465e60504
commit
81467e6f35
47 changed files with 736 additions and 16 deletions
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue