github.com/geraldss/go/src@v0.0.0-20210511222824-ac7d0ebfc235/html/template/js.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package template 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "reflect" 12 "strings" 13 "unicode/utf8" 14 ) 15 16 // nextJSCtx returns the context that determines whether a slash after the 17 // given run of tokens starts a regular expression instead of a division 18 // operator: / or /=. 19 // 20 // This assumes that the token run does not include any string tokens, comment 21 // tokens, regular expression literal tokens, or division operators. 22 // 23 // This fails on some valid but nonsensical JavaScript programs like 24 // "x = ++/foo/i" which is quite different than "x++/foo/i", but is not known to 25 // fail on any known useful programs. It is based on the draft 26 // JavaScript 2.0 lexical grammar and requires one token of lookbehind: 27 // https://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html 28 func nextJSCtx(s []byte, preceding jsCtx) jsCtx { 29 s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029") 30 if len(s) == 0 { 31 return preceding 32 } 33 34 // All cases below are in the single-byte UTF-8 group. 35 switch c, n := s[len(s)-1], len(s); c { 36 case '+', '-': 37 // ++ and -- are not regexp preceders, but + and - are whether 38 // they are used as infix or prefix operators. 39 start := n - 1 40 // Count the number of adjacent dashes or pluses. 41 for start > 0 && s[start-1] == c { 42 start-- 43 } 44 if (n-start)&1 == 1 { 45 // Reached for trailing minus signs since "---" is the 46 // same as "-- -". 47 return jsCtxRegexp 48 } 49 return jsCtxDivOp 50 case '.': 51 // Handle "42." 52 if n != 1 && '0' <= s[n-2] && s[n-2] <= '9' { 53 return jsCtxDivOp 54 } 55 return jsCtxRegexp 56 // Suffixes for all punctuators from section 7.7 of the language spec 57 // that only end binary operators not handled above. 58 case ',', '<', '>', '=', '*', '%', '&', '|', '^', '?': 59 return jsCtxRegexp 60 // Suffixes for all punctuators from section 7.7 of the language spec 61 // that are prefix operators not handled above. 62 case '!', '~': 63 return jsCtxRegexp 64 // Matches all the punctuators from section 7.7 of the language spec 65 // that are open brackets not handled above. 66 case '(', '[': 67 return jsCtxRegexp 68 // Matches all the punctuators from section 7.7 of the language spec 69 // that precede expression starts. 70 case ':', ';', '{': 71 return jsCtxRegexp 72 // CAVEAT: the close punctuators ('}', ']', ')') precede div ops and 73 // are handled in the default except for '}' which can precede a 74 // division op as in 75 // ({ valueOf: function () { return 42 } } / 2 76 // which is valid, but, in practice, developers don't divide object 77 // literals, so our heuristic works well for code like 78 // function () { ... } /foo/.test(x) && sideEffect(); 79 // The ')' punctuator can precede a regular expression as in 80 // if (b) /foo/.test(x) && ... 81 // but this is much less likely than 82 // (a + b) / c 83 case '}': 84 return jsCtxRegexp 85 default: 86 // Look for an IdentifierName and see if it is a keyword that 87 // can precede a regular expression. 88 j := n 89 for j > 0 && isJSIdentPart(rune(s[j-1])) { 90 j-- 91 } 92 if regexpPrecederKeywords[string(s[j:])] { 93 return jsCtxRegexp 94 } 95 } 96 // Otherwise is a punctuator not listed above, or 97 // a string which precedes a div op, or an identifier 98 // which precedes a div op. 99 return jsCtxDivOp 100 } 101 102 // regexpPrecederKeywords is a set of reserved JS keywords that can precede a 103 // regular expression in JS source. 104 var regexpPrecederKeywords = map[string]bool{ 105 "break": true, 106 "case": true, 107 "continue": true, 108 "delete": true, 109 "do": true, 110 "else": true, 111 "finally": true, 112 "in": true, 113 "instanceof": true, 114 "return": true, 115 "throw": true, 116 "try": true, 117 "typeof": true, 118 "void": true, 119 } 120 121 var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() 122 123 // indirectToJSONMarshaler returns the value, after dereferencing as many times 124 // as necessary to reach the base type (or nil) or an implementation of json.Marshal. 125 func indirectToJSONMarshaler(a interface{}) interface{} { 126 // text/template now supports passing untyped nil as a func call 127 // argument, so we must support it. Otherwise we'd panic below, as one 128 // cannot call the Type or Interface methods on an invalid 129 // reflect.Value. See golang.org/issue/18716. 130 if a == nil { 131 return nil 132 } 133 134 v := reflect.ValueOf(a) 135 for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Ptr && !v.IsNil() { 136 v = v.Elem() 137 } 138 return v.Interface() 139 } 140 141 // jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has 142 // neither side-effects nor free variables outside (NaN, Infinity). 143 func jsValEscaper(args ...interface{}) string { 144 var a interface{} 145 if len(args) == 1 { 146 a = indirectToJSONMarshaler(args[0]) 147 switch t := a.(type) { 148 case JS: 149 return string(t) 150 case JSStr: 151 // TODO: normalize quotes. 152 return `"` + string(t) + `"` 153 case json.Marshaler: 154 // Do not treat as a Stringer. 155 case fmt.Stringer: 156 a = t.String() 157 } 158 } else { 159 for i, arg := range args { 160 args[i] = indirectToJSONMarshaler(arg) 161 } 162 a = fmt.Sprint(args...) 163 } 164 // TODO: detect cycles before calling Marshal which loops infinitely on 165 // cyclic data. This may be an unacceptable DoS risk. 166 b, err := json.Marshal(a) 167 if err != nil { 168 // Put a space before comment so that if it is flush against 169 // a division operator it is not turned into a line comment: 170 // x/{{y}} 171 // turning into 172 // x//* error marshaling y: 173 // second line of error message */null 174 return fmt.Sprintf(" /* %s */null ", strings.ReplaceAll(err.Error(), "*/", "* /")) 175 } 176 177 // TODO: maybe post-process output to prevent it from containing 178 // "<!--", "-->", "<![CDATA[", "]]>", or "</script" 179 // in case custom marshalers produce output containing those. 180 // Note: Do not use \x escaping to save bytes because it is not JSON compatible and this escaper 181 // supports ld+json content-type. 182 if len(b) == 0 { 183 // In, `x=y/{{.}}*z` a json.Marshaler that produces "" should 184 // not cause the output `x=y/*z`. 185 return " null " 186 } 187 first, _ := utf8.DecodeRune(b) 188 last, _ := utf8.DecodeLastRune(b) 189 var buf strings.Builder 190 // Prevent IdentifierNames and NumericLiterals from running into 191 // keywords: in, instanceof, typeof, void 192 pad := isJSIdentPart(first) || isJSIdentPart(last) 193 if pad { 194 buf.WriteByte(' ') 195 } 196 written := 0 197 // Make sure that json.Marshal escapes codepoints U+2028 & U+2029 198 // so it falls within the subset of JSON which is valid JS. 199 for i := 0; i < len(b); { 200 rune, n := utf8.DecodeRune(b[i:]) 201 repl := "" 202 if rune == 0x2028 { 203 repl = `\u2028` 204 } else if rune == 0x2029 { 205 repl = `\u2029` 206 } 207 if repl != "" { 208 buf.Write(b[written:i]) 209 buf.WriteString(repl) 210 written = i + n 211 } 212 i += n 213 } 214 if buf.Len() != 0 { 215 buf.Write(b[written:]) 216 if pad { 217 buf.WriteByte(' ') 218 } 219 return buf.String() 220 } 221 return string(b) 222 } 223 224 // jsStrEscaper produces a string that can be included between quotes in 225 // JavaScript source, in JavaScript embedded in an HTML5 <script> element, 226 // or in an HTML5 event handler attribute such as onclick. 227 func jsStrEscaper(args ...interface{}) string { 228 s, t := stringify(args...) 229 if t == contentTypeJSStr { 230 return replace(s, jsStrNormReplacementTable) 231 } 232 return replace(s, jsStrReplacementTable) 233 } 234 235 // jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression 236 // specials so the result is treated literally when included in a regular 237 // expression literal. /foo{{.X}}bar/ matches the string "foo" followed by 238 // the literal text of {{.X}} followed by the string "bar". 239 func jsRegexpEscaper(args ...interface{}) string { 240 s, _ := stringify(args...) 241 s = replace(s, jsRegexpReplacementTable) 242 if s == "" { 243 // /{{.X}}/ should not produce a line comment when .X == "". 244 return "(?:)" 245 } 246 return s 247 } 248 249 // replace replaces each rune r of s with replacementTable[r], provided that 250 // r < len(replacementTable). If replacementTable[r] is the empty string then 251 // no replacement is made. 252 // It also replaces runes U+2028 and U+2029 with the raw strings `\u2028` and 253 // `\u2029`. 254 func replace(s string, replacementTable []string) string { 255 var b strings.Builder 256 r, w, written := rune(0), 0, 0 257 for i := 0; i < len(s); i += w { 258 // See comment in htmlEscaper. 259 r, w = utf8.DecodeRuneInString(s[i:]) 260 var repl string 261 switch { 262 case int(r) < len(lowUnicodeReplacementTable): 263 repl = lowUnicodeReplacementTable[r] 264 case int(r) < len(replacementTable) && replacementTable[r] != "": 265 repl = replacementTable[r] 266 case r == '\u2028': 267 repl = `\u2028` 268 case r == '\u2029': 269 repl = `\u2029` 270 default: 271 continue 272 } 273 if written == 0 { 274 b.Grow(len(s)) 275 } 276 b.WriteString(s[written:i]) 277 b.WriteString(repl) 278 written = i + w 279 } 280 if written == 0 { 281 return s 282 } 283 b.WriteString(s[written:]) 284 return b.String() 285 } 286 287 var lowUnicodeReplacementTable = []string{ 288 0: `\u0000`, 1: `\u0001`, 2: `\u0002`, 3: `\u0003`, 4: `\u0004`, 5: `\u0005`, 6: `\u0006`, 289 '\a': `\u0007`, 290 '\b': `\u0008`, 291 '\t': `\t`, 292 '\n': `\n`, 293 '\v': `\u000b`, // "\v" == "v" on IE 6. 294 '\f': `\f`, 295 '\r': `\r`, 296 0xe: `\u000e`, 0xf: `\u000f`, 0x10: `\u0010`, 0x11: `\u0011`, 0x12: `\u0012`, 0x13: `\u0013`, 297 0x14: `\u0014`, 0x15: `\u0015`, 0x16: `\u0016`, 0x17: `\u0017`, 0x18: `\u0018`, 0x19: `\u0019`, 298 0x1a: `\u001a`, 0x1b: `\u001b`, 0x1c: `\u001c`, 0x1d: `\u001d`, 0x1e: `\u001e`, 0x1f: `\u001f`, 299 } 300 301 var jsStrReplacementTable = []string{ 302 0: `\u0000`, 303 '\t': `\t`, 304 '\n': `\n`, 305 '\v': `\u000b`, // "\v" == "v" on IE 6. 306 '\f': `\f`, 307 '\r': `\r`, 308 // Encode HTML specials as hex so the output can be embedded 309 // in HTML attributes without further encoding. 310 '"': `\u0022`, 311 '&': `\u0026`, 312 '\'': `\u0027`, 313 '+': `\u002b`, 314 '/': `\/`, 315 '<': `\u003c`, 316 '>': `\u003e`, 317 '\\': `\\`, 318 } 319 320 // jsStrNormReplacementTable is like jsStrReplacementTable but does not 321 // overencode existing escapes since this table has no entry for `\`. 322 var jsStrNormReplacementTable = []string{ 323 0: `\u0000`, 324 '\t': `\t`, 325 '\n': `\n`, 326 '\v': `\u000b`, // "\v" == "v" on IE 6. 327 '\f': `\f`, 328 '\r': `\r`, 329 // Encode HTML specials as hex so the output can be embedded 330 // in HTML attributes without further encoding. 331 '"': `\u0022`, 332 '&': `\u0026`, 333 '\'': `\u0027`, 334 '+': `\u002b`, 335 '/': `\/`, 336 '<': `\u003c`, 337 '>': `\u003e`, 338 } 339 var jsRegexpReplacementTable = []string{ 340 0: `\u0000`, 341 '\t': `\t`, 342 '\n': `\n`, 343 '\v': `\u000b`, // "\v" == "v" on IE 6. 344 '\f': `\f`, 345 '\r': `\r`, 346 // Encode HTML specials as hex so the output can be embedded 347 // in HTML attributes without further encoding. 348 '"': `\u0022`, 349 '$': `\$`, 350 '&': `\u0026`, 351 '\'': `\u0027`, 352 '(': `\(`, 353 ')': `\)`, 354 '*': `\*`, 355 '+': `\u002b`, 356 '-': `\-`, 357 '.': `\.`, 358 '/': `\/`, 359 '<': `\u003c`, 360 '>': `\u003e`, 361 '?': `\?`, 362 '[': `\[`, 363 '\\': `\\`, 364 ']': `\]`, 365 '^': `\^`, 366 '{': `\{`, 367 '|': `\|`, 368 '}': `\}`, 369 } 370 371 // isJSIdentPart reports whether the given rune is a JS identifier part. 372 // It does not handle all the non-Latin letters, joiners, and combining marks, 373 // but it does handle every codepoint that can occur in a numeric literal or 374 // a keyword. 375 func isJSIdentPart(r rune) bool { 376 switch { 377 case r == '$': 378 return true 379 case '0' <= r && r <= '9': 380 return true 381 case 'A' <= r && r <= 'Z': 382 return true 383 case r == '_': 384 return true 385 case 'a' <= r && r <= 'z': 386 return true 387 } 388 return false 389 } 390 391 // isJSType reports whether the given MIME type should be considered JavaScript. 392 // 393 // It is used to determine whether a script tag with a type attribute is a javascript container. 394 func isJSType(mimeType string) bool { 395 // per 396 // https://www.w3.org/TR/html5/scripting-1.html#attr-script-type 397 // https://tools.ietf.org/html/rfc7231#section-3.1.1 398 // https://tools.ietf.org/html/rfc4329#section-3 399 // https://www.ietf.org/rfc/rfc4627.txt 400 // discard parameters 401 if i := strings.Index(mimeType, ";"); i >= 0 { 402 mimeType = mimeType[:i] 403 } 404 mimeType = strings.ToLower(mimeType) 405 mimeType = strings.TrimSpace(mimeType) 406 switch mimeType { 407 case 408 "application/ecmascript", 409 "application/javascript", 410 "application/json", 411 "application/ld+json", 412 "application/x-ecmascript", 413 "application/x-javascript", 414 "module", 415 "text/ecmascript", 416 "text/javascript", 417 "text/javascript1.0", 418 "text/javascript1.1", 419 "text/javascript1.2", 420 "text/javascript1.3", 421 "text/javascript1.4", 422 "text/javascript1.5", 423 "text/jscript", 424 "text/livescript", 425 "text/x-ecmascript", 426 "text/x-javascript": 427 return true 428 default: 429 return false 430 } 431 }