Current section

14 Versions

Jump to

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 {?<, "&lt;"},
  @@ -16,65 +20,76 @@ defmodule Combo.SafeHTML.Escape do
16 20 {?', "&#39;"}
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…