Current section
14 Versions
Jump to
Current section
14 Versions
Compare versions
23
files changed
+344
additions
-190
deletions
| @@ -1,9 +1,9 @@ | |
| 1 1 | {<<"links">>, |
| 2 2 | [{<<"Source">>,<<"https://github.com/combo-lab/combo">>}, |
| 3 3 | {<<"Changelog">>, |
| 4 | - <<"https://github.com/combo-lab/combo/blob/v0.10.0/CHANGELOG.md">>}]}. |
| 4 | + <<"https://github.com/combo-lab/combo/blob/v0.10.1/CHANGELOG.md">>}]}. |
| 5 5 | {<<"name">>,<<"combo">>}. |
| 6 | - {<<"version">>,<<"0.10.0">>}. |
| 6 | + {<<"version">>,<<"0.10.1">>}. |
| 7 7 | {<<"description">>, |
| 8 8 | <<"A web framework, that combines the good parts of modern web development.">>}. |
| 9 9 | {<<"elixir">>,<<"~> 1.18">>}. |
| @@ -111,7 +111,7 @@ | |
| 111 111 | [[{<<"name">>,<<"plug">>}, |
| 112 112 | {<<"app">>,<<"plug">>}, |
| 113 113 | {<<"optional">>,false}, |
| 114 | - {<<"requirement">>,<<"~> 1.14">>}, |
| 114 | + {<<"requirement">>,<<"~> 1.19">>}, |
| 115 115 | {<<"repository">>,<<"hexpm">>}], |
| 116 116 | [{<<"name">>,<<"plug_crypto">>}, |
| 117 117 | {<<"app">>,<<"plug_crypto">>}, |
| @@ -146,12 +146,12 @@ | |
| 146 146 | [{<<"name">>,<<"plug_cowboy">>}, |
| 147 147 | {<<"app">>,<<"plug_cowboy">>}, |
| 148 148 | {<<"optional">>,true}, |
| 149 | - {<<"requirement">>,<<"~> 2.7">>}, |
| 149 | + {<<"requirement">>,<<"~> 2.8">>}, |
| 150 150 | {<<"repository">>,<<"hexpm">>}], |
| 151 151 | [{<<"name">>,<<"bandit">>}, |
| 152 152 | {<<"app">>,<<"bandit">>}, |
| 153 153 | {<<"optional">>,true}, |
| 154 | - {<<"requirement">>,<<"~> 1.0">>}, |
| 154 | + {<<"requirement">>,<<"~> 1.11">>}, |
| 155 155 | {<<"repository">>,<<"hexpm">>}], |
| 156 156 | [{<<"name">>,<<"jason">>}, |
| 157 157 | {<<"app">>,<<"jason">>}, |
| @@ -161,6 +161,6 @@ | |
| 161 161 | [{<<"name">>,<<"decimal">>}, |
| 162 162 | {<<"app">>,<<"decimal">>}, |
| 163 163 | {<<"optional">>,true}, |
| 164 | - {<<"requirement">>,<<"~> 2.0">>}, |
| 164 | + {<<"requirement">>,<<"~> 3.0">>}, |
| 165 165 | {<<"repository">>,<<"hexpm">>}]]}. |
| 166 166 | {<<"build_tools">>,[<<"mix">>]}. |
| @@ -76,7 +76,7 @@ defmodule Combo.FilteredParams do | |
| 76 76 | |
| 77 77 | defp keep(_other, _keys), do: replacement() |
| 78 78 | |
| 79 | - @default_rule {:discard, ["password"]} |
| 79 | + @default_rule {:discard, ["password", "token"]} |
| 80 80 | defp rule do |
| 81 81 | Combo.Env.get_env(:filtered_params, :rule, @default_rule) |
| 82 82 | end |
| @@ -90,7 +90,6 @@ defmodule Combo.HTML.Components do | |
| 90 90 | data-method={if @method != "get", do: @method} |
| 91 91 | data-csrf={if @method != "get", do: csrf_token(@csrf_token, @href)} |
| 92 92 | data-to={if @method != "get", do: @href} |
| 93 | - ceex-no-format |
| 94 93 | {@rest} |
| 95 94 | >{render_slot(@inner_block)}</a> |
| 96 95 | """ |
| @@ -10,6 +10,7 @@ defmodule Combo.SafeHTML do | |
| 10 10 | - ... |
| 11 11 | """ |
| 12 12 | |
| 13 | + import Bitwise, only: [&&&: 2] |
| 13 14 | alias Combo.SafeHTML.Safe |
| 14 15 | alias Combo.SafeHTML.Escape |
| 15 16 | |
| @@ -92,7 +93,7 @@ defmodule Combo.SafeHTML do | |
| 92 93 | |
| 93 94 | """ |
| 94 95 | @spec escape(String.t()) :: String.t() |
| 95 | - defdelegate escape(string), to: Escape, as: :escape_html |
| 96 | + def escape(string) when is_binary(string), do: Escape.escape_binary(string) |
| 96 97 | |
| 97 98 | @doc ~S""" |
| 98 99 | Escapes an enumerable of attributes, returning iodata. |
| @@ -125,17 +126,94 @@ defmodule Combo.SafeHTML do | |
| 125 126 | |
| 126 127 | """ |
| 127 128 | @spec escape_attrs([{term(), term()}] | map()) :: iodata() |
| 128 | - defdelegate escape_attrs(list_or_map), to: Escape |
| 129 | + def escape_attrs(attrs) when is_list(attrs) do |
| 130 | + build_attrs(attrs) |
| 131 | + end |
| 129 132 | |
| 130 | - @doc """ |
| 131 | - Escapes a term as the key of an attribute. |
| 132 | - """ |
| 133 | - @spec escape_attr_key(term()) :: iodata() |
| 134 | - defdelegate escape_attr_key(term), to: Escape, as: :escape_key |
| 133 | + def escape_attrs(attrs) do |
| 134 | + attrs |> Enum.to_list() |> build_attrs() |
| 135 | + end |
| 135 136 | |
| 136 | - @doc """ |
| 137 | - Escapes a term as the value of an attribute. |
| 138 | - """ |
| 139 | - @spec escape_attr_value(term()) :: iodata() |
| 140 | - defdelegate escape_attr_value(term), to: Escape, as: :escape_value |
| 137 | + defp build_attrs([{k, true} | t]), |
| 138 | + do: [?\s, escape_attr_name(k) | build_attrs(t)] |
| 139 | + |
| 140 | + defp build_attrs([{_, false} | t]), |
| 141 | + do: build_attrs(t) |
| 142 | + |
| 143 | + defp build_attrs([{_, nil} | t]), |
| 144 | + do: build_attrs(t) |
| 145 | + |
| 146 | + defp build_attrs([{k, v} | t]), |
| 147 | + do: [?\s, escape_attr_name(k), ?=, ?", escape_attr_value(v), ?" | build_attrs(t)] |
| 148 | + |
| 149 | + defp build_attrs([]), do: [] |
| 150 | + |
| 151 | + defp escape_attr_name(atom) when is_atom(atom) do |
| 152 | + atom |> Atom.to_string() |> validate_attr_name!() |
| 153 | + end |
| 154 | + |
| 155 | + defp escape_attr_name(string) when is_binary(string) do |
| 156 | + validate_attr_name!(string) |
| 157 | + end |
| 158 | + |
| 159 | + defp escape_attr_name(other) do |
| 160 | + raise ArgumentError, |
| 161 | + "expected attribute name to be an atom or string, got: #{inspect(other)}" |
| 162 | + end |
| 163 | + |
| 164 | + defp validate_attr_name!(name) do |
| 165 | + if valid_attr_name?(name) do |
| 166 | + name |
| 167 | + else |
| 168 | + raise ArgumentError, |
| 169 | + "expected attribute name to be a non-empty atom or string containing valid characters, " <> |
| 170 | + "got: #{inspect(name)}" |
| 171 | + end |
| 172 | + end |
| 173 | + |
| 174 | + defp escape_attr_value(term), do: Safe.to_iodata(term) |
| 175 | + |
| 176 | + # https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 |
| 177 | + # |
| 178 | + # Attribute names must consist of one or more characters other than: |
| 179 | + # |
| 180 | + # * controls |
| 181 | + # * U+0020 SPACE |
| 182 | + # * syntax characters: U+0022 ("), U+0027 ('), U+003E (>), U+002F (/), U+003D (=) |
| 183 | + # * noncharacters |
| 184 | + # |
| 185 | + defp valid_attr_name?(""), do: false |
| 186 | + defp valid_attr_name?(name), do: valid_attr_name_chars?(name) |
| 187 | + |
| 188 | + defp valid_attr_name_chars?(<<c::utf8, rest::binary>>) do |
| 189 | + not control?(c) and |
| 190 | + not space?(c) and |
| 191 | + not syntax_char?(c) and |
| 192 | + not nonchar?(c) and |
| 193 | + valid_attr_name_chars?(rest) |
| 194 | + end |
| 195 | + |
| 196 | + defp valid_attr_name_chars?(<<>>), do: true |
| 197 | + |
| 198 | + # * C0 control - 0x00..0x1F |
| 199 | + # * DEL - 0x7F |
| 200 | + # * C1 control - 0x80..0x9F |
| 201 | + defp control?(c) do |
| 202 | + (c >= 0x0000 and c <= 0x001F) or c == 0x007F or (c >= 0x0080 and c <= 0x009F) |
| 203 | + end |
| 204 | + |
| 205 | + defp space?(c) do |
| 206 | + c == 0x0020 |
| 207 | + end |
| 208 | + |
| 209 | + # ?< is not required by the spec, but is included as an additional safeguard. |
| 210 | + @syntax_chars [?<, ?>, ?", ?', ?/, ?=] |
| 211 | + defp syntax_char?(c) do |
| 212 | + c in @syntax_chars |
| 213 | + end |
| 214 | + |
| 215 | + defp nonchar?(c) do |
| 216 | + (c >= 0xFDD0 and c <= 0xFDEF) or |
| 217 | + (c &&& 0xFFFE) == 0xFFFE |
| 218 | + end |
| 141 219 | end |
| @@ -1,12 +1,16 @@ | |
| 1 1 | defmodule Combo.SafeHTML.Escape do |
| 2 2 | @moduledoc false |
| 3 3 | |
| 4 | - alias Combo.SafeHTML.Safe |
| 5 | - |
| 6 | - @doc false |
| 7 | - def escape_html(bin) when is_binary(bin) do |
| 8 | - escape_html(bin, 0, bin, []) |
| 9 | - end |
| 4 | + # For text nodes, `<`, `>`, and `&` need escaping. |
| 5 | + # For attribute values, `<`, `>`, `&`, `"`, and `'` need escaping. |
| 6 | + # |
| 7 | + # We could provide different escaping logic for each, but depending on the |
| 8 | + # implementation, that might mean lots of duplicated code or complex macros. |
| 9 | + # |
| 10 | + # So I decided to use the same escaping logic for both — escape `<`, `>`, |
| 11 | + # `&`, `"`, and `'` regardless of whether it's a text node or attribute value. |
| 12 | + # |
| 13 | + # Escaping `"` and `'` in text nodes is harmless, they still render correctly. |
| 10 14 | |
| 11 15 | escapes = [ |
| 12 16 | {?<, "<"}, |
| @@ -16,65 +20,76 @@ defmodule Combo.SafeHTML.Escape do | |
| 16 20 | {?', "'"} |
| 17 21 | ] |
| 18 22 | |
| 23 | + # Escaping binaries |
| 24 | + |
| 25 | + @doc false |
| 26 | + def escape_binary(binary) when is_binary(binary), do: escape_binary(binary, 0, binary, []) |
| 27 | + |
| 19 28 | for {match, insert} <- escapes do |
| 20 | - defp escape_html(<<unquote(match), rest::bits>>, skip, original, acc) do |
| 21 | - escape_html(rest, skip + 1, original, [acc, unquote(insert)]) |
| 29 | + defp escape_binary(<<unquote(match), rest::bits>>, skip, original, acc) do |
| 30 | + escape_binary(rest, skip + 1, original, [acc, unquote(insert)]) |
| 22 31 | end |
| 23 32 | end |
| 24 33 | |
| 25 | - defp escape_html(<<_char, rest::bits>>, skip, original, acc) do |
| 26 | - escape_html(rest, skip, original, acc, 1) |
| 34 | + defp escape_binary(<<_char, rest::bits>>, skip, original, acc) do |
| 35 | + escape_binary(rest, skip, original, acc, 1) |
| 27 36 | end |
| 28 37 | |
| 29 | - defp escape_html(<<>>, _skip, _original, acc) do |
| 38 | + defp escape_binary(<<>>, _skip, _original, acc) do |
| 30 39 | acc |
| 31 40 | end |
| 32 41 | |
| 33 42 | for {match, insert} <- escapes do |
| 34 | - defp escape_html(<<unquote(match), rest::bits>>, skip, original, acc, len) do |
| 43 | + defp escape_binary(<<unquote(match), rest::bits>>, skip, original, acc, len) do |
| 35 44 | part = binary_part(original, skip, len) |
| 36 | - escape_html(rest, skip + len + 1, original, [acc, part, unquote(insert)]) |
| 45 | + escape_binary(rest, skip + len + 1, original, [acc, part, unquote(insert)]) |
| 37 46 | end |
| 38 47 | end |
| 39 48 | |
| 40 | - defp escape_html(<<_char, rest::bits>>, skip, original, acc, len) do |
| 41 | - escape_html(rest, skip, original, acc, len + 1) |
| 49 | + defp escape_binary(<<_char, rest::bits>>, skip, original, acc, len) do |
| 50 | + escape_binary(rest, skip, original, acc, len + 1) |
| 42 51 | end |
| 43 52 | |
| 44 | - defp escape_html(<<>>, 0, original, _acc, _len) do |
| 53 | + defp escape_binary(<<>>, 0, original, _acc, _len) do |
| 45 54 | original |
| 46 55 | end |
| 47 56 | |
| 48 | - defp escape_html(<<>>, skip, original, acc, len) do |
| 57 | + defp escape_binary(<<>>, skip, original, acc, len) do |
| 49 58 | [acc, binary_part(original, skip, len)] |
| 50 59 | end |
| 51 60 | |
| 61 | + # Escaping lists |
| 62 | + |
| 52 63 | @doc false |
| 53 | - def escape_attrs(attrs) when is_list(attrs) do |
| 54 | - build_attrs(attrs) |
| 64 | + def escape_list(list) when is_list(list), do: escape_list_elem(list) |
| 65 | + |
| 66 | + ## bytes |
| 67 | + for {match, insert} <- escapes do |
| 68 | + defp escape_list_elem(unquote(match)), do: unquote(insert) |
| 55 69 | end |
| 56 70 | |
| 57 | - def escape_attrs(attrs) do |
| 58 | - attrs |> Enum.to_list() |> build_attrs() |
| 71 | + defp escape_list_elem(h) when is_integer(h) and h >= 0 and h <= 255 do |
| 72 | + h |
| 59 73 | end |
| 60 74 | |
| 61 | - defp build_attrs([{k, true} | t]), |
| 62 | - do: [?\s, escape_key(k) | build_attrs(t)] |
| 75 | + ## binaries |
| 76 | + defp escape_list_elem(h) when is_binary(h) do |
| 77 | + escape_binary(h) |
| 78 | + end |
| 63 79 | |
| 64 | - defp build_attrs([{_, false} | t]), |
| 65 | - do: build_attrs(t) |
| 80 | + ## lists |
| 81 | + defp escape_list_elem([h | t]), do: [escape_list_elem(h) | escape_list_elem(t)] |
| 82 | + defp escape_list_elem([]), do: [] |
| 66 83 | |
| 67 | - defp build_attrs([{_, nil} | t]), |
| 68 | - do: build_attrs(t) |
| 84 | + ## safe data |
| 85 | + defp escape_list_elem({:safe, data}) do |
| 86 | + data |
| 87 | + end |
| 69 88 | |
| 70 | - defp build_attrs([{k, v} | t]), |
| 71 | - do: [?\s, escape_key(k), ?=, ?", escape_value(v), ?" | build_attrs(t)] |
| 72 | - |
| 73 | - defp build_attrs([]), do: [] |
| 74 | - |
| 75 | - @doc false |
| 76 | - def escape_key(value), do: Safe.to_iodata(value) |
| 77 | - |
| 78 | - @doc false |
| 79 | - def escape_value(value), do: Safe.to_iodata(value) |
| 89 | + ## fallback |
| 90 | + defp escape_list_elem(other) do |
| 91 | + raise ArgumentError, |
| 92 | + "expected list element to be a byte (0-255), binary, list or {:safe, data}, " <> |
| 93 | + "got: #{inspect(other)}" |
| 94 | + end |
| 80 95 | end |
Loading more files…