zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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