From d8d490d574f7ffdd191add1f3f7c1e8747764d1c Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Thu, 8 Dec 2016 14:23:58 +0000 Subject: [PATCH] feat: Support automatic escaping HTML --- Sources/Extension.swift | 1 + Sources/Filters.swift | 5 +++++ Sources/HTML.swift | 26 ++++++++++++++++++++++++++ Sources/Node.swift | 8 ++++++-- Tests/StencilTests/FilterSpec.swift | 9 +++++++++ Tests/StencilTests/NodeSpec.swift | 13 +++++++++++++ docs/builtins.rst | 17 +++++++++++++++++ 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Sources/HTML.swift diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 9dfa879a..dd658b5c 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -57,6 +57,7 @@ class DefaultExtension: Extension { registerFilter("uppercase", filter: uppercase) registerFilter("lowercase", filter: lowercase) registerFilter("join", filter: joinFilter) + registerFilter("safe", filter: safeFilter) } } diff --git a/Sources/Filters.swift b/Sources/Filters.swift index dd78ff42..a1bb23cc 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -39,3 +39,8 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? { return value } + + +func safeFilter(value: Any?) throws -> Any? { + return escaped(html: stringify(value)) +} diff --git a/Sources/HTML.swift b/Sources/HTML.swift new file mode 100644 index 00000000..630c52b3 --- /dev/null +++ b/Sources/HTML.swift @@ -0,0 +1,26 @@ +public protocol HTMLString { + var html: String { get } +} + + +struct EscapedHTML: HTMLString, CustomStringConvertible { + let value: String + + var html: String { return value } + var description: String { return value } +} + + +func escaped(html: String) -> HTMLString { + return EscapedHTML(value: html) +} + + +func escapeHTML(_ value: String) -> String { + return value + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "'", with: "&39;") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "\"", with: """) +} diff --git a/Sources/Node.swift b/Sources/Node.swift index 5b47177a..9881b802 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -70,11 +70,15 @@ public class VariableNode : NodeType { public func render(_ context: Context) throws -> String { let result = try variable.resolve(context) - return stringify(result) + + if let result = result as? EscapedHTML { + return result.html + } + + return escapeHTML(stringify(result)) } } - func stringify(_ result: Any?) -> String { if let result = result as? String { return result diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 3f86cb39..29864775 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -147,4 +147,13 @@ func testFilter() { try expect(result) == "OneTwo" } } + + describe("safe filter") { + let template = Template(templateString: "{{ \"<'>\"|safe }}") + + $0.it("prevents auto escaping") { + let result = try template.render() + try expect(result) == "<'>" + } + } } diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index 431d225d..a2ac5657 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -12,6 +12,7 @@ class ErrorNode : NodeType { func testNode() { describe("Node") { let context = Context(dictionary: [ + "title": escaped(html: "'Hello World'"), "name": "Kyle", "age": 27, "items": [1, 2, 3], @@ -34,6 +35,18 @@ func testNode() { let node = VariableNode(variable: Variable("age")) try expect(try node.render(context)) == "27" } + + $0.describe("escaping") { + $0.it("automatically escapes unescaped html") { + let node = VariableNode(variable: Variable("\"'Hello World'\"")) + try expect(try node.render(context)) == "&39;Hello World&39;" + } + + $0.it("doesn't double escape already escaped HTML") { + let node = VariableNode(variable: Variable("title")) + try expect(try node.render(context)) == "'Hello World'" + } + } } $0.describe("rendering nodes") { diff --git a/docs/builtins.rst b/docs/builtins.rst index b4657c87..214f2f2c 100644 --- a/docs/builtins.rst +++ b/docs/builtins.rst @@ -286,3 +286,20 @@ Join an array of items. {{ value|join:", " }} .. note:: The value MUST be an array. + +``safe`` +~~~~~~~~ + +Marks a value as safe and thus prevents the value from being automatically +escaped. + +.. code-block:: html+django + + {{ name|safe }} + +Other filters may remove the safe state, for example filtering through `safe` +and then `lowercase` will result in an unsafe lowercased string. + +.. code-block:: html+django + + {{ name|safe|lowercase }}