query.ex (7670B)
1 defmodule Plug.Conn.Query do 2 @moduledoc """ 3 Conveniences for decoding and encoding URL-encoded queries. 4 5 Plug allows developers to build query strings that map to 6 Elixir structures in order to make manipulation of such structures 7 easier on the server side. Here are some examples: 8 9 iex> decode("foo=bar")["foo"] 10 "bar" 11 12 If a value is given more than once, the last value takes precedence: 13 14 iex> decode("foo=bar&foo=baz")["foo"] 15 "baz" 16 17 Nested structures can be created via `[key]`: 18 19 iex> decode("foo[bar]=baz")["foo"]["bar"] 20 "baz" 21 22 Lists are created with `[]`: 23 24 iex> decode("foo[]=bar&foo[]=baz")["foo"] 25 ["bar", "baz"] 26 27 Keys without values are treated as empty strings, 28 according to https://url.spec.whatwg.org/#application/x-www-form-urlencoded: 29 30 iex> decode("foo")["foo"] 31 "" 32 33 Maps can be encoded: 34 35 iex> encode(%{foo: "bar", baz: "bat"}) 36 "baz=bat&foo=bar" 37 38 Encoding keyword lists preserves the order of the fields: 39 40 iex> encode([foo: "bar", baz: "bat"]) 41 "foo=bar&baz=bat" 42 43 When encoding keyword lists with duplicate keys, the key that comes first 44 takes precedence: 45 46 iex> encode([foo: "bar", foo: "bat"]) 47 "foo=bar" 48 49 Encoding named lists: 50 51 iex> encode(%{foo: ["bar", "baz"]}) 52 "foo[]=bar&foo[]=baz" 53 54 Encoding nested structures: 55 56 iex> encode(%{foo: %{bar: "baz"}}) 57 "foo[bar]=baz" 58 59 """ 60 61 @doc """ 62 Decodes the given `query`. 63 64 The `query` is assumed to be encoded in the "x-www-form-urlencoded" format. 65 The format is decoded at first. Then, if `validate_utf8` is `true`, the decoded 66 result is validated for proper UTF-8 encoding. 67 68 `initial` is the initial "accumulator" where decoded values will be added. 69 70 `invalid_exception` is the exception module for the exception to raise on 71 errors with decoding. 72 """ 73 @spec decode(String.t(), map(), module(), boolean()) :: %{optional(String.t()) => term()} 74 def decode( 75 query, 76 initial \\ %{}, 77 invalid_exception \\ Plug.Conn.InvalidQueryError, 78 validate_utf8 \\ true 79 ) 80 81 def decode("", initial, _invalid_exception, _validate_utf8) do 82 initial 83 end 84 85 def decode(query, initial, invalid_exception, validate_utf8) 86 when is_binary(query) do 87 parts = :binary.split(query, "&", [:global]) 88 89 Enum.reduce( 90 Enum.reverse(parts), 91 initial, 92 &decode_www_pair(&1, &2, invalid_exception, validate_utf8) 93 ) 94 end 95 96 defp decode_www_pair("", acc, _invalid_exception, _validate_utf8) do 97 acc 98 end 99 100 defp decode_www_pair(binary, acc, invalid_exception, validate_utf8) do 101 current = 102 case :binary.split(binary, "=") do 103 [key, value] -> 104 {decode_www_form(key, invalid_exception, validate_utf8), 105 decode_www_form(value, invalid_exception, validate_utf8)} 106 107 [key] -> 108 {decode_www_form(key, invalid_exception, validate_utf8), ""} 109 end 110 111 decode_pair(current, acc) 112 end 113 114 defp decode_www_form(value, invalid_exception, validate_utf8) do 115 # TODO: Remove rescue as this can't fail from Elixir v1.13 116 try do 117 URI.decode_www_form(value) 118 rescue 119 ArgumentError -> 120 raise invalid_exception, "invalid urlencoded params, got #{value}" 121 else 122 binary -> 123 if validate_utf8 do 124 Plug.Conn.Utils.validate_utf8!(binary, invalid_exception, "urlencoded params") 125 end 126 127 binary 128 end 129 end 130 131 @doc """ 132 Decodes the given tuple and stores it in the given accumulator. 133 134 It parses the key and stores the value into the current 135 accumulator. The keys and values are not assumed to be 136 encoded in "x-www-form-urlencoded". 137 138 Parameter lists are added to the accumulator in reverse 139 order, so be sure to pass the parameters in reverse order. 140 """ 141 @spec decode_pair({String.t(), term()}, acc) :: acc when acc: term() 142 def decode_pair({key, value} = _pair, acc) do 143 if key != "" and :binary.last(key) == ?] do 144 # Remove trailing ] 145 subkey = :binary.part(key, 0, byte_size(key) - 1) 146 147 # Split the first [ then we will split on remaining ][. 148 # 149 # users[address][street #=> [ "users", "address][street" ] 150 # 151 assign_split(:binary.split(subkey, "["), value, acc, :binary.compile_pattern("][")) 152 else 153 assign_map(acc, key, value) 154 end 155 end 156 157 defp assign_split(["", rest], value, acc, pattern) do 158 parts = :binary.split(rest, pattern) 159 160 case acc do 161 [_ | _] -> [assign_split(parts, value, :none, pattern) | acc] 162 :none -> [assign_split(parts, value, :none, pattern)] 163 _ -> acc 164 end 165 end 166 167 defp assign_split([key, rest], value, acc, pattern) do 168 parts = :binary.split(rest, pattern) 169 170 case acc do 171 %{^key => current} when is_list(current) or is_map(current) -> 172 Map.put(acc, key, assign_split(parts, value, current, pattern)) 173 174 %{^key => _} -> 175 acc 176 177 %{} -> 178 Map.put(acc, key, assign_split(parts, value, :none, pattern)) 179 180 _ -> 181 %{key => assign_split(parts, value, :none, pattern)} 182 end 183 end 184 185 defp assign_split([""], nil, acc, _pattern) do 186 case acc do 187 [_ | _] -> acc 188 _ -> [] 189 end 190 end 191 192 defp assign_split([""], value, acc, _pattern) do 193 case acc do 194 [_ | _] -> [value | acc] 195 :none -> [value] 196 _ -> acc 197 end 198 end 199 200 defp assign_split([key], value, acc, _pattern) do 201 assign_map(acc, key, value) 202 end 203 204 defp assign_map(acc, key, value) do 205 case acc do 206 %{^key => _} -> acc 207 %{} -> Map.put(acc, key, value) 208 _ -> %{key => value} 209 end 210 end 211 212 @doc """ 213 Encodes the given map or list of tuples. 214 """ 215 @spec encode(Enumerable.t(), (term() -> binary())) :: binary() 216 def encode(kv, encoder \\ &to_string/1) do 217 IO.iodata_to_binary(encode_pair("", kv, encoder)) 218 end 219 220 # covers structs 221 defp encode_pair(field, %{__struct__: struct} = map, encoder) when is_atom(struct) do 222 [field, ?= | encode_value(map, encoder)] 223 end 224 225 # covers maps 226 defp encode_pair(parent_field, %{} = map, encoder) do 227 encode_kv(map, parent_field, encoder) 228 end 229 230 # covers keyword lists 231 defp encode_pair(parent_field, list, encoder) when is_list(list) and is_tuple(hd(list)) do 232 encode_kv(Enum.uniq_by(list, &elem(&1, 0)), parent_field, encoder) 233 end 234 235 # covers non-keyword lists 236 defp encode_pair(parent_field, list, encoder) when is_list(list) do 237 mapper = fn 238 value when is_map(value) and map_size(value) != 1 -> 239 raise ArgumentError, 240 "cannot encode maps inside lists when the map has 0 or more than 1 element, " <> 241 "got: #{inspect(value)}" 242 243 value -> 244 [?&, encode_pair(parent_field <> "[]", value, encoder)] 245 end 246 247 list 248 |> Enum.flat_map(mapper) 249 |> prune() 250 end 251 252 # covers nil 253 defp encode_pair(field, nil, _encoder) do 254 [field, ?=] 255 end 256 257 # encoder fallback 258 defp encode_pair(field, value, encoder) do 259 [field, ?= | encode_value(value, encoder)] 260 end 261 262 defp encode_kv(kv, parent_field, encoder) do 263 mapper = fn 264 {_, value} when value in [%{}, []] -> 265 [] 266 267 {field, value} -> 268 field = 269 if parent_field == "" do 270 encode_key(field) 271 else 272 parent_field <> "[" <> encode_key(field) <> "]" 273 end 274 275 [?&, encode_pair(field, value, encoder)] 276 end 277 278 kv 279 |> Enum.flat_map(mapper) 280 |> prune() 281 end 282 283 defp encode_key(item) do 284 item |> to_string |> URI.encode_www_form() 285 end 286 287 defp encode_value(item, encoder) do 288 item |> encoder.() |> URI.encode_www_form() 289 end 290 291 defp prune([?& | t]), do: t 292 defp prune([]), do: [] 293 end