package htmlsanitize import ( "bytes" "encoding/json" "html" "io" "mime" "strings" ) // String escapes text before it is returned to browser-facing management clients. func String(value string) string { return html.EscapeString(value) } // Strings escapes each string in values while preserving order. func Strings(values []string) []string { out := make([]string, 0, len(values)) for _, value := range values { out = append(out, String(value)) } return out } // JSONBody escapes all string values in a JSON document. func JSONBody(body []byte) ([]byte, bool) { trimmed := bytes.TrimSpace(body) if len(trimmed) == 0 { return body, false } decoder := json.NewDecoder(bytes.NewReader(trimmed)) decoder.UseNumber() var value any if errDecode := decoder.Decode(&value); errDecode != nil { return body, false } var extra any if errExtra := decoder.Decode(&extra); errExtra != io.EOF { return body, false } var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) encoder.SetEscapeHTML(false) if errEncode := encoder.Encode(JSONValue(value)); errEncode != nil { return body, false } return bytes.TrimSuffix(buffer.Bytes(), []byte("\n")), true } // JSONBodyIfLikely escapes JSON bodies when the content type or body shape indicates JSON. func JSONBodyIfLikely(body []byte, contentType string) ([]byte, bool) { if IsJSONContentType(contentType) || LooksLikeJSON(body) { return JSONBody(body) } return body, false } // JSONValue recursively escapes string values in JSON-compatible data. func JSONValue(value any) any { switch typed := value.(type) { case string: return String(typed) case []any: out := make([]any, len(typed)) for index, item := range typed { out[index] = JSONValue(item) } return out case map[string]any: out := make(map[string]any, len(typed)) for key, item := range typed { out[key] = JSONValue(item) } return out default: return value } } // IsJSONContentType reports whether contentType is application/json or a +json type. func IsJSONContentType(contentType string) bool { mediaType, _, errParse := mime.ParseMediaType(strings.TrimSpace(contentType)) if errParse != nil { mediaType = strings.TrimSpace(contentType) } mediaType = strings.ToLower(mediaType) return mediaType == "application/json" || strings.HasSuffix(mediaType, "+json") } // LooksLikeJSON reports whether body starts with an object or array JSON marker. func LooksLikeJSON(body []byte) bool { trimmed := bytes.TrimSpace(body) if len(trimmed) == 0 { return false } return trimmed[0] == '{' || trimmed[0] == '[' }