From 807620c80c0ecb157ab579922144e102cc5a3a32 Mon Sep 17 00:00:00 2001 From: bert hubert <68055427+berthubert@users.noreply.github.com> Date: Sat, 5 Oct 2024 09:08:46 +0200 Subject: [PATCH] Add HTML autoescape (#292) * add and document set_html_autoescape * add render_to to Environment that accepts a string (and turns it into a Template) * code style, update single include * update ci * revert macos-14 test --------- Co-authored-by: pantor --- .github/workflows/ci.yml | 12 ++++++------ README.md | 8 ++++++++ include/inja/config.hpp | 1 + include/inja/environment.hpp | 9 +++++++++ include/inja/renderer.hpp | 22 +++++++++++++++++++++- single_include/inja/inja.hpp | 34 ++++++++++++++++++++++++++++++++-- 6 files changed, 77 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5acf89a..dd6bd88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,18 +56,18 @@ jobs: os: windows-2022 compiler: msvc - - name: macOS-11-gcc - os: macOS-11 + - name: macOS-12-gcc + os: macOS-12 compiler: gcc - - name: macOS-11-clang - os: macOS-11 - compiler: clang - - name: macOS-12-clang os: macOS-12 compiler: clang + # - name: macOS-14-clang + # os: macOS-14 + # compiler: clang + steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index a45cc56..c03f841 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ env.set_expression("{{", "}}"); // Expressions env.set_comment("{#", "#}"); // Comments env.set_statement("{%", "%}"); // Statements {% %} for many things, see below env.set_line_statement("##"); // Line statements ## (just an opener) +env.set_html_autoescape(true); // Perform HTML escaping on all strings ``` ### Variables @@ -364,6 +365,13 @@ render("{% if neighbour in guests -%} I was there{% endif -%} !", data); // Stripping behind a statement or expression also removes any newlines. +### HTML escaping + +Templates are frequently used to creat HTML pages. Source data that contains +characters that have meaning within HTML (like <. >, &) needs to be escaped. +It is often inconvenient to perform such escaping within the JSON data. With `Environment::set_html_autoescape(true)`, Inja can be configured to +HTML escape each and every string created. + ### Comments Comments can be written with the `{# ... #}` syntax. diff --git a/include/inja/config.hpp b/include/inja/config.hpp index 0a8f9b7..f81487b 100644 --- a/include/inja/config.hpp +++ b/include/inja/config.hpp @@ -74,6 +74,7 @@ struct ParserConfig { */ struct RenderConfig { bool throw_at_missing_includes {true}; + bool html_autoescape {false}; }; } // namespace inja diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index fdb2962..e36b6a4 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -93,6 +93,11 @@ public: render_config.throw_at_missing_includes = will_throw; } + /// Sets whether we'll automatically perform HTML escape + void set_html_autoescape(bool will_escape) { + render_config.html_autoescape = will_escape; + } + Template parse(std::string_view input) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return parser.parse(input, input_path); @@ -155,6 +160,10 @@ public: return os; } + std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) { + return render_to(os, parse(input), data); + } + std::string load_file(const std::string& filename) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return Parser::load_file(input_path + filename); diff --git a/include/inja/renderer.hpp b/include/inja/renderer.hpp index efd936e..6b8b05e 100644 --- a/include/inja/renderer.hpp +++ b/include/inja/renderer.hpp @@ -53,9 +53,29 @@ class Renderer : public NodeVisitor { return !data->empty(); } + static std::string htmlescape(const std::string& data) { + std::string buffer; + buffer.reserve(1.1 * data.size()); + for (size_t pos = 0; pos != data.size(); ++pos) { + switch (data[pos]) { + case '&': buffer.append("&"); break; + case '\"': buffer.append("""); break; + case '\'': buffer.append("'"); break; + case '<': buffer.append("<"); break; + case '>': buffer.append(">"); break; + default: buffer.append(&data[pos], 1); break; + } + } + return buffer; + } + void print_data(const std::shared_ptr value) { if (value->is_string()) { - *output_stream << value->get_ref(); + if (config.html_autoescape) { + *output_stream << htmlescape(value->get_ref()); + } else { + *output_stream << value->get_ref(); + } } else if (value->is_number_unsigned()) { *output_stream << value->get(); } else if (value->is_number_integer()) { diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index 6c62e8d..1d8d711 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -884,6 +884,7 @@ struct ParserConfig { */ struct RenderConfig { bool throw_at_missing_includes {true}; + bool html_autoescape {false}; }; } // namespace inja @@ -2124,9 +2125,29 @@ class Renderer : public NodeVisitor { return !data->empty(); } + static std::string htmlescape(const std::string& data) { + std::string buffer; + buffer.reserve(1.1 * data.size()); + for (size_t pos = 0; pos != data.size(); ++pos) { + switch (data[pos]) { + case '&': buffer.append("&"); break; + case '\"': buffer.append("""); break; + case '\'': buffer.append("'"); break; + case '<': buffer.append("<"); break; + case '>': buffer.append(">"); break; + default: buffer.append(&data[pos], 1); break; + } + } + return buffer; + } + void print_data(const std::shared_ptr value) { if (value->is_string()) { - *output_stream << value->get_ref(); + if (config.html_autoescape) { + *output_stream << htmlescape(value->get_ref()); + } else { + *output_stream << value->get_ref(); + } } else if (value->is_number_unsigned()) { *output_stream << value->get(); } else if (value->is_number_integer()) { @@ -2382,7 +2403,7 @@ class Renderer : public NodeVisitor { } break; case Op::Capitalize: { auto result = get_arguments<1>(node)[0]->get(); - result[0] = std::toupper(result[0]); + result[0] = static_cast(::toupper(result[0])); std::transform(result.begin() + 1, result.end(), result.begin() + 1, [](char c) { return static_cast(::tolower(c)); }); make_result(std::move(result)); } break; @@ -2792,6 +2813,11 @@ public: render_config.throw_at_missing_includes = will_throw; } + /// Sets whether we'll automatically perform HTML escape + void set_html_autoescape(bool will_escape) { + render_config.html_autoescape = will_escape; + } + Template parse(std::string_view input) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return parser.parse(input, input_path); @@ -2854,6 +2880,10 @@ public: return os; } + std::ostream& render_to(std::ostream& os, const std::string_view input, const json& data) { + return render_to(os, parse(input), data); + } + std::string load_file(const std::string& filename) { Parser parser(parser_config, lexer_config, template_storage, function_storage); return Parser::load_file(input_path + filename);