diff options
Diffstat (limited to 'src/html/template/css.go')
-rw-r--r-- | src/html/template/css.go | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/src/html/template/css.go b/src/html/template/css.go new file mode 100644 index 0000000..eb92fc9 --- /dev/null +++ b/src/html/template/css.go @@ -0,0 +1,260 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package template + +import ( + "bytes" + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// endsWithCSSKeyword reports whether b ends with an ident that +// case-insensitively matches the lower-case kw. +func endsWithCSSKeyword(b []byte, kw string) bool { + i := len(b) - len(kw) + if i < 0 { + // Too short. + return false + } + if i != 0 { + r, _ := utf8.DecodeLastRune(b[:i]) + if isCSSNmchar(r) { + // Too long. + return false + } + } + // Many CSS keywords, such as "!important" can have characters encoded, + // but the URI production does not allow that according to + // https://www.w3.org/TR/css3-syntax/#TOK-URI + // This does not attempt to recognize encoded keywords. For example, + // given "\75\72\6c" and "url" this return false. + return string(bytes.ToLower(b[i:])) == kw +} + +// isCSSNmchar reports whether rune is allowed anywhere in a CSS identifier. +func isCSSNmchar(r rune) bool { + // Based on the CSS3 nmchar production but ignores multi-rune escape + // sequences. + // https://www.w3.org/TR/css3-syntax/#SUBTOK-nmchar + return 'a' <= r && r <= 'z' || + 'A' <= r && r <= 'Z' || + '0' <= r && r <= '9' || + r == '-' || + r == '_' || + // Non-ASCII cases below. + 0x80 <= r && r <= 0xd7ff || + 0xe000 <= r && r <= 0xfffd || + 0x10000 <= r && r <= 0x10ffff +} + +// decodeCSS decodes CSS3 escapes given a sequence of stringchars. +// If there is no change, it returns the input, otherwise it returns a slice +// backed by a new array. +// https://www.w3.org/TR/css3-syntax/#SUBTOK-stringchar defines stringchar. +func decodeCSS(s []byte) []byte { + i := bytes.IndexByte(s, '\\') + if i == -1 { + return s + } + // The UTF-8 sequence for a codepoint is never longer than 1 + the + // number hex digits need to represent that codepoint, so len(s) is an + // upper bound on the output length. + b := make([]byte, 0, len(s)) + for len(s) != 0 { + i := bytes.IndexByte(s, '\\') + if i == -1 { + i = len(s) + } + b, s = append(b, s[:i]...), s[i:] + if len(s) < 2 { + break + } + // https://www.w3.org/TR/css3-syntax/#SUBTOK-escape + // escape ::= unicode | '\' [#x20-#x7E#x80-#xD7FF#xE000-#xFFFD#x10000-#x10FFFF] + if isHex(s[1]) { + // https://www.w3.org/TR/css3-syntax/#SUBTOK-unicode + // unicode ::= '\' [0-9a-fA-F]{1,6} wc? + j := 2 + for j < len(s) && j < 7 && isHex(s[j]) { + j++ + } + r := hexDecode(s[1:j]) + if r > unicode.MaxRune { + r, j = r/16, j-1 + } + n := utf8.EncodeRune(b[len(b):cap(b)], r) + // The optional space at the end allows a hex + // sequence to be followed by a literal hex. + // string(decodeCSS([]byte(`\A B`))) == "\nB" + b, s = b[:len(b)+n], skipCSSSpace(s[j:]) + } else { + // `\\` decodes to `\` and `\"` to `"`. + _, n := utf8.DecodeRune(s[1:]) + b, s = append(b, s[1:1+n]...), s[1+n:] + } + } + return b +} + +// isHex reports whether the given character is a hex digit. +func isHex(c byte) bool { + return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' +} + +// hexDecode decodes a short hex digit sequence: "10" -> 16. +func hexDecode(s []byte) rune { + n := '\x00' + for _, c := range s { + n <<= 4 + switch { + case '0' <= c && c <= '9': + n |= rune(c - '0') + case 'a' <= c && c <= 'f': + n |= rune(c-'a') + 10 + case 'A' <= c && c <= 'F': + n |= rune(c-'A') + 10 + default: + panic(fmt.Sprintf("Bad hex digit in %q", s)) + } + } + return n +} + +// skipCSSSpace returns a suffix of c, skipping over a single space. +func skipCSSSpace(c []byte) []byte { + if len(c) == 0 { + return c + } + // wc ::= #x9 | #xA | #xC | #xD | #x20 + switch c[0] { + case '\t', '\n', '\f', ' ': + return c[1:] + case '\r': + // This differs from CSS3's wc production because it contains a + // probable spec error whereby wc contains all the single byte + // sequences in nl (newline) but not CRLF. + if len(c) >= 2 && c[1] == '\n' { + return c[2:] + } + return c[1:] + } + return c +} + +// isCSSSpace reports whether b is a CSS space char as defined in wc. +func isCSSSpace(b byte) bool { + switch b { + case '\t', '\n', '\f', '\r', ' ': + return true + } + return false +} + +// cssEscaper escapes HTML and CSS special characters using \<hex>+ escapes. +func cssEscaper(args ...interface{}) string { + s, _ := stringify(args...) + var b strings.Builder + r, w, written := rune(0), 0, 0 + for i := 0; i < len(s); i += w { + // See comment in htmlEscaper. + r, w = utf8.DecodeRuneInString(s[i:]) + var repl string + switch { + case int(r) < len(cssReplacementTable) && cssReplacementTable[r] != "": + repl = cssReplacementTable[r] + default: + continue + } + if written == 0 { + b.Grow(len(s)) + } + b.WriteString(s[written:i]) + b.WriteString(repl) + written = i + w + if repl != `\\` && (written == len(s) || isHex(s[written]) || isCSSSpace(s[written])) { + b.WriteByte(' ') + } + } + if written == 0 { + return s + } + b.WriteString(s[written:]) + return b.String() +} + +var cssReplacementTable = []string{ + 0: `\0`, + '\t': `\9`, + '\n': `\a`, + '\f': `\c`, + '\r': `\d`, + // Encode HTML specials as hex so the output can be embedded + // in HTML attributes without further encoding. + '"': `\22`, + '&': `\26`, + '\'': `\27`, + '(': `\28`, + ')': `\29`, + '+': `\2b`, + '/': `\2f`, + ':': `\3a`, + ';': `\3b`, + '<': `\3c`, + '>': `\3e`, + '\\': `\\`, + '{': `\7b`, + '}': `\7d`, +} + +var expressionBytes = []byte("expression") +var mozBindingBytes = []byte("mozbinding") + +// cssValueFilter allows innocuous CSS values in the output including CSS +// quantities (10px or 25%), ID or class literals (#foo, .bar), keyword values +// (inherit, blue), and colors (#888). +// It filters out unsafe values, such as those that affect token boundaries, +// and anything that might execute scripts. +func cssValueFilter(args ...interface{}) string { + s, t := stringify(args...) + if t == contentTypeCSS { + return s + } + b, id := decodeCSS([]byte(s)), make([]byte, 0, 64) + + // CSS3 error handling is specified as honoring string boundaries per + // https://www.w3.org/TR/css3-syntax/#error-handling : + // Malformed declarations. User agents must handle unexpected + // tokens encountered while parsing a declaration by reading until + // the end of the declaration, while observing the rules for + // matching pairs of (), [], {}, "", and '', and correctly handling + // escapes. For example, a malformed declaration may be missing a + // property, colon (:) or value. + // So we need to make sure that values do not have mismatched bracket + // or quote characters to prevent the browser from restarting parsing + // inside a string that might embed JavaScript source. + for i, c := range b { + switch c { + case 0, '"', '\'', '(', ')', '/', ';', '@', '[', '\\', ']', '`', '{', '}': + return filterFailsafe + case '-': + // Disallow <!-- or -->. + // -- should not appear in valid identifiers. + if i != 0 && b[i-1] == '-' { + return filterFailsafe + } + default: + if c < utf8.RuneSelf && isCSSNmchar(rune(c)) { + id = append(id, c) + } + } + } + id = bytes.ToLower(id) + if bytes.Contains(id, expressionBytes) || bytes.Contains(id, mozBindingBytes) { + return filterFailsafe + } + return string(b) +} |