1defmodule Plug.Parsers do
2  defmodule RequestTooLargeError do
3    @moduledoc """
4    Error raised when the request is too large.
5    """
6
7    defexception message: "the request is too large. If you are willing to process " <>
8                          "larger requests, please give a :length to Plug.Parsers",
9                 plug_status: 413
10  end
11
12  defmodule UnsupportedMediaTypeError do
13    @moduledoc """
14    Error raised when the request body cannot be parsed.
15    """
16
17    defexception media_type: nil, plug_status: 415
18
19    def message(exception) do
20      "unsupported media type #{exception.media_type}"
21    end
22  end
23
24  defmodule BadEncodingError do
25    @moduledoc """
26    Raised when the request body contains bad encoding.
27    """
28
29    defexception message: nil, plug_status: 415
30  end
31
32  defmodule ParseError do
33    @moduledoc """
34    Error raised when the request body is malformed.
35    """
36
37    defexception exception: nil, plug_status: 400
38
39    def message(%{exception: exception}) do
40      "malformed request, a #{inspect exception.__struct__} exception was raised " <>
41        "with message #{inspect(Exception.message(exception))}"
42    end
43  end
44
45  @moduledoc """
46  A plug for parsing the request body.
47
48  This module also specifies a behaviour that all the parsers to be used with
49  Plug should adopt.
50
51  This plug also fetches query params in the connection through
52  `Plug.Conn.fetch_query_params/2`.
53
54  Once a connection goes through this plug, it will have `:body_params` set to
55  the map of params parsed by one of the parsers listed in `:parsers` and
56  `:params` set to the result of merging the `:body_params` and `:query_params`.
57
58  This plug will raise `Plug.Parsers.UnsupportedMediaTypeError` by default if
59  the request cannot be parsed by any of the given types and the MIME type has
60  not been explicity accepted with the `:pass` option.
61
62  `Plug.Parsers.RequestTooLargeError` will be raised if the request goes over
63  the given limit.
64
65  Parsers may raise a `Plug.Parsers.ParseError` if the request has a malformed
66  body.
67
68  This plug only parses the body if the request method is one of the following:
69
70    * `POST`
71    * `PUT`
72    * `PATCH`
73    * `DELETE`
74
75  For requests with a different request method, this plug will only fetch the
76  query params.
77
78  ## Options
79
80    * `:parsers` - a list of modules to be invoked for parsing.
81      These modules need to implement the behaviour outlined in
82      this module.
83
84    * `:pass` - an optional list of MIME type strings that are allowed
85      to pass through. Any mime not handled by a parser and not explicitly
86      listed in `:pass` will `raise UnsupportedMediaTypeError`. For example:
87
88        * `["*/*"]` - never raises
89        * `["text/html", "application/*"]` - doesn't raise for those values
90        * `[]` - always raises (default)
91
92    * `:query_string_length` - the maximum allowed size for query strings
93
94  ## Examples
95
96      plug Plug.Parsers, parsers: [:urlencoded, :multipart]
97
98      plug Plug.Parsers, parsers: [:urlencoded, :json],
99                         pass:  ["text/*"],
100                         json_decoder: Poison
101
102  Each parser also accepts options to be given directly to it by using tuples.
103  For example, to support file uploads it is common pass the `:length`,
104  `:read_length` and `:read_timeout` option to the multipart parser:
105
106      plug Plug.Parsers,
107           parsers: [
108             :url_encoded,
109             {:multipart, length: 20_000_000} # Increase to 20MB max upload
110           ]
111
112  ## Built-in parsers
113
114  Plug ships with the following parsers:
115
116    * `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded`
117      requests (can be used as `:urlencoded` as well in the `:parsers` option)
118    * `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and
119      `multipart/mixed` requests (can be used as `:multipart` as well in the
120      `:parsers` option)
121    * `Plug.Parsers.JSON` - parses `application/json` requests with the given
122      `:json_decoder` (can be used as `:json` as well in the `:parsers` option)
123
124  ## File handling
125
126  If a file is uploaded via any of the parsers, Plug will
127  stream the uploaded contents to a file in a temporary directory in order to
128  avoid loading the whole file into memory. For such, the `:plug` application
129  needs to be started in order for file uploads to work. More details on how the
130  uploaded file is handled can be found in the documentation for `Plug.Upload`.
131
132  When a file is uploaded, the request parameter that identifies that file will
133  be a `Plug.Upload` struct with information about the uploaded file (e.g.
134  filename and content type) and about where the file is stored.
135
136  The temporary directory where files are streamed to can be customized by
137  setting the `PLUG_TMPDIR` environment variable on the host system. If
138  `PLUG_TMPDIR` isn't set, Plug will look at some environment
139  variables which usually hold the value of the system's temporary directory
140  (like `TMPDIR` or `TMP`). If no value is found in any of those variables,
141  `/tmp` is used as a default.
142  """
143
144  alias Plug.Conn
145
146  @callback init(opts :: Keyword.t) :: Plug.opts
147
148  @doc """
149  Attempts to parse the connection's request body given the content-type type,
150  subtype, and its parameters.
151
152  The arguments are:
153
154    * the `Plug.Conn` connection
155    * `type`, the content-type type (e.g., `"x-sample"` for the
156      `"x-sample/json"` content-type)
157    * `subtype`, the content-type subtype (e.g., `"json"` for the
158      `"x-sample/json"` content-type)
159    * `params`, the content-type parameters (e.g., `%{"foo" => "bar"}`
160      for the `"text/plain; foo=bar"` content-type)
161
162  This function should return:
163
164    * `{:ok, body_params, conn}` if the parser is able to handle the given
165      content-type; `body_params` should be a map
166    * `{:next, conn}` if the next parser should be invoked
167    * `{:error, :too_large, conn}` if the request goes over the given limit
168
169  """
170  @callback parse(conn :: Conn.t, type :: binary, subtype :: binary,
171                  params :: Keyword.t, state :: term) ::
172                  {:ok, Conn.params, Conn.t} |
173                  {:error, :too_large, Conn.t} |
174                  {:next, Conn.t}
175
176  # TODO: Remove me
177  @optional_callbacks [init: 1]
178
179  @behaviour Plug
180  @methods ~w(POST PUT PATCH DELETE)
181
182  def init(opts) do
183    {parsers, opts} = Keyword.pop(opts, :parsers)
184    {pass, opts} = Keyword.pop(opts, :pass, [])
185    {query_string_length, opts} = Keyword.pop(opts, :query_string_length, 1_000_000)
186
187    unless parsers do
188      raise ArgumentError, "Plug.Parsers expects a set of parsers to be given in :parsers"
189    end
190
191    {convert_parsers(parsers, opts), pass, query_string_length}
192  end
193
194  defp convert_parsers(parsers, root_opts) do
195    for parser <- parsers do
196      {parser, opts} =
197        case parser do
198          {parser, opts} when is_atom(parser) and is_list(opts) ->
199            {parser, Keyword.merge(root_opts, opts)}
200          parser when is_atom(parser) ->
201            {parser, root_opts}
202        end
203
204      module =
205        case Atom.to_string(parser) do
206          "Elixir." <> _ -> parser
207          reference -> Module.concat(Plug.Parsers, String.upcase(reference))
208        end
209
210      if Code.ensure_compiled?(module) and function_exported?(module, :init, 1) do
211        {module, module.init(opts)}
212      else
213        {module, opts}
214      end
215    end
216  end
217
218  def call(%{method: method, body_params: %Plug.Conn.Unfetched{}} = conn, options)
219      when method in @methods do
220    {parsers, pass, query_string_length} = options
221    %{req_headers: req_headers} = conn
222    conn = Conn.fetch_query_params(conn, length: query_string_length)
223
224    case List.keyfind(req_headers, "content-type", 0) do
225      {"content-type", ct} ->
226        case Conn.Utils.content_type(ct) do
227          {:ok, type, subtype, params} ->
228            reduce(conn, parsers, type, subtype, params, pass, query_string_length)
229          :error ->
230            merge_params(conn, %{}, query_string_length)
231        end
232      nil ->
233        merge_params(conn, %{}, query_string_length)
234    end
235  end
236
237  def call(%{body_params: body_params} = conn, {_, _, query_string_length}) do
238    merge_params(conn, make_empty_if_unfetched(body_params), query_string_length)
239  end
240
241  defp reduce(conn, [{parser, options} | rest], type, subtype, params, pass, query_string_length) do
242    case parser.parse(conn, type, subtype, params, options) do
243      {:ok, body, conn} ->
244        merge_params(conn, body, query_string_length)
245      {:next, conn} ->
246        reduce(conn, rest, type, subtype, params, pass, query_string_length)
247      {:error, :too_large, _conn} ->
248        raise RequestTooLargeError
249    end
250  end
251
252  defp reduce(conn, [], type, subtype, _params, pass, _query_string_length) do
253    ensure_accepted_mimes(conn, type, subtype, pass)
254  end
255
256  defp ensure_accepted_mimes(conn, _type, _subtype, ["*/*"]), do: conn
257  defp ensure_accepted_mimes(conn, type, subtype, pass) do
258    if "#{type}/#{subtype}" in pass || "#{type}/*" in pass do
259      conn
260    else
261      raise UnsupportedMediaTypeError, media_type: "#{type}/#{subtype}"
262    end
263  end
264
265  defp merge_params(conn, body_params, query_string_length) do
266    %{params: params, path_params: path_params} = conn
267    params = make_empty_if_unfetched(params)
268    conn = Plug.Conn.fetch_query_params(conn, length: query_string_length)
269    params =
270      conn.query_params
271      |> Map.merge(params)
272      |> Map.merge(body_params)
273      |> Map.merge(path_params)
274    %{conn | params: params, body_params: body_params}
275  end
276
277  defp make_empty_if_unfetched(%Plug.Conn.Unfetched{}), do: %{}
278  defp make_empty_if_unfetched(params), do: params
279end
280