// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of "dart:convert"; /// A `String` converter that converts characters to HTML entities. /// /// This is intended to sanitize text before inserting the text into an HTML /// document. Characters that are meaningful in HTML are converted to /// HTML entities (like `&` for `&`). /// /// The general converter escapes all characters that are meaningful in HTML /// attributes or normal element context. Elements with special content types /// (like CSS or JavaScript) may need a more specialized escaping that /// understands that content type. /// /// If the context where the text will be inserted is known in more detail, /// it's possible to omit escaping some characters (like quotes when not /// inside an attribute value). /// /// The escaped text should only be used inside quoted HTML attributes values /// or as text content of a normal element. Using the escaped text inside a /// tag, but not inside a quoted attribute value, is still dangerous. const HtmlEscape htmlEscape = HtmlEscape(); /// HTML escape modes. /// /// Allows specifying a mode for HTML escaping that depends on the context /// where the escaped result is going to be used. /// The relevant contexts are: /// /// * as text content of an HTML element. /// * as value of a (single- or double-) quoted attribute value. /// /// All modes require escaping of `&` (ampersand) characters, and may /// enable escaping of more characters. /// /// Custom escape modes can be created using the [HtmlEscapeMode.new] /// constructor. /// /// Example: /// ```dart /// const htmlEscapeMode = HtmlEscapeMode( /// name: 'custom', /// escapeLtGt: true, /// escapeQuot: false, /// escapeApos: false, /// escapeSlash: false, /// ); /// /// const HtmlEscape htmlEscape = HtmlEscape(htmlEscapeMode); /// String unescaped = 'Text & subject'; /// String escaped = htmlEscape.convert(unescaped); /// print(escaped); // Text & subject /// /// unescaped = '10 > 1 and 1 < 10'; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // 10 > 1 and 1 < 10 /// /// unescaped = "Single-quoted: 'text'"; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // Single-quoted: 'text' /// /// unescaped = 'Double-quoted: "text"'; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // Double-quoted: "text" /// /// unescaped = 'Path: /system/'; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // Path: /system/ /// ``` final class HtmlEscapeMode { final String _name; /// Whether to escape '<' and '>'. final bool escapeLtGt; /// Whether to escape '"' (quote). final bool escapeQuot; /// Whether to escape "'" (apostrophe). final bool escapeApos; /// Whether to escape "/" (forward slash, solidus). /// /// Escaping a slash is recommended to avoid cross-site scripting attacks by /// [the Open Web Application Security Project](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content) final bool escapeSlash; /// Default escaping mode, which escapes all characters. /// /// The result of such an escaping is usable both in element content and /// in any attribute value. /// /// The escaping only works for elements with normal HTML content, /// and not, for example, for script or style element content, /// which require escapes matching their particular content syntax. static const HtmlEscapeMode unknown = HtmlEscapeMode._( 'unknown', true, true, true, true, ); /// Escaping mode for text going into double-quoted HTML attribute values. /// /// The result should not be used as the content of an unquoted /// or single-quoted attribute value. /// /// Escapes double quotes (`"`) but not single quotes (`'`), /// and escapes `<` and `>` characters because they are not allowed /// in strict XHTML attributes static const HtmlEscapeMode attribute = HtmlEscapeMode._( 'attribute', true, true, false, false, ); /// Escaping mode for text going into single-quoted HTML attribute values. /// /// The result should not be used as the content of an unquoted /// or double-quoted attribute value. /// /// Escapes single quotes (`'`) but not double quotes (`"`), /// and escapes `<` and `>` characters because they are not allowed /// in strict XHTML attributes. static const HtmlEscapeMode sqAttribute = HtmlEscapeMode._( 'attribute', true, false, true, false, ); /// Escaping mode for text going into HTML element content. /// /// The escaping only works for elements with normal HTML content, /// and not, for example, for script or style element content, /// which require escapes matching their particular content syntax. /// /// Escapes `<` and `>` characters. static const HtmlEscapeMode element = HtmlEscapeMode._( 'element', true, false, false, false, ); const HtmlEscapeMode._( this._name, this.escapeLtGt, this.escapeQuot, this.escapeApos, this.escapeSlash, ); /// Create a custom escaping mode. /// /// All modes escape `&`. /// The mode can further be set to escape `<` and `>` ([escapeLtGt]), /// `"` ([escapeQuot]), `'` ([escapeApos]), and/or `/` ([escapeSlash]). const HtmlEscapeMode({ String name = "custom", this.escapeLtGt = false, this.escapeQuot = false, this.escapeApos = false, this.escapeSlash = false, }) : _name = name; String toString() => _name; } /// Converter which escapes characters with special meaning in HTML. /// /// The converter finds characters that are significant in the HTML source and /// replaces them with corresponding HTML entities. /// /// The characters that need escaping in HTML are: /// /// * `&` (ampersand) always needs to be escaped. /// * `<` (less than) and `>` (greater than) when inside an element. /// * `"` (quote) when inside a double-quoted attribute value. /// * `'` (apostrophe) when inside a single-quoted attribute value. /// Apostrophe is escaped as `'` instead of `'` since /// not all browsers understand `'`. /// * `/` (slash) is recommended to be escaped because it may be used /// to terminate an element in some HTML dialects. /// /// Escaping `>` (greater than) isn't necessary, but the result is often /// found to be easier to read if greater-than is also escaped whenever /// less-than is. /// /// Example: /// ```dart /// const HtmlEscape htmlEscape = HtmlEscape(); /// String unescaped = 'Text & subject'; /// String escaped = htmlEscape.convert(unescaped); /// print(escaped); // Text & subject /// /// unescaped = '10 > 1 and 1 < 10'; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // 10 > 1 and 1 < 10 /// /// unescaped = "Single-quoted: 'text'"; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // Single-quoted: 'text' /// /// unescaped = 'Double-quoted: "text"'; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // Double-quoted: "text" /// /// unescaped = 'Path: /system/'; /// escaped = htmlEscape.convert(unescaped); /// print(escaped); // Path: /system/ /// ``` final class HtmlEscape extends Converter { /// The [HtmlEscapeMode] used by the converter. final HtmlEscapeMode mode; /// Create converter that escapes HTML characters. /// /// If [mode] is provided as either [HtmlEscapeMode.attribute] or /// [HtmlEscapeMode.element], only the corresponding subset of HTML /// characters is escaped. /// The default is to escape all HTML characters. const HtmlEscape([this.mode = HtmlEscapeMode.unknown]); String convert(String text) { var val = _convert(text, 0, text.length); return val == null ? text : val; } /// Converts the substring of text from start to end. /// /// Returns `null` if no changes were necessary, otherwise returns /// the converted string. String? _convert(String text, int start, int end) { StringBuffer? result; for (var i = start; i < end; i++) { var ch = text[i]; String? replacement; switch (ch) { case '&': replacement = '&'; break; case '"': if (mode.escapeQuot) replacement = '"'; break; case "'": if (mode.escapeApos) replacement = '''; break; case '<': if (mode.escapeLtGt) replacement = '<'; break; case '>': if (mode.escapeLtGt) replacement = '>'; break; case '/': if (mode.escapeSlash) replacement = '/'; break; } if (replacement != null) { result ??= StringBuffer(); if (i > start) result.write(text.substring(start, i)); result.write(replacement); start = i + 1; } } if (result == null) return null; if (end > start) result.write(text.substring(start, end)); return result.toString(); } StringConversionSink startChunkedConversion(Sink sink) { return _HtmlEscapeSink( this, sink is StringConversionSink ? sink : StringConversionSink.from(sink), ); } } class _HtmlEscapeSink extends StringConversionSink { final HtmlEscape _escape; final StringConversionSink _sink; _HtmlEscapeSink(this._escape, this._sink); void addSlice(String chunk, int start, int end, bool isLast) { var val = _escape._convert(chunk, start, end); if (val == null) { _sink.addSlice(chunk, start, end, isLast); } else { _sink.add(val); if (isLast) _sink.close(); } } void close() { _sink.close(); } }