First
[anni] / lib / pleroma / web / api_spec / cast_and_validate.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
3 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
4 # SPDX-License-Identifier: AGPL-3.0-only
5
6 defmodule Pleroma.Web.ApiSpec.CastAndValidate do
7   @moduledoc """
8   This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
9   (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
10   The main difference is ignoring unexpected query params instead of throwing
11   an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
12   to disable this behavior. Also, the default rendering error module
13   is `Pleroma.Web.ApiSpec.RenderError`.
14   """
15
16   @behaviour Plug
17
18   alias OpenApiSpex.Plug.PutApiSpec
19   alias Plug.Conn
20
21   @impl Plug
22   def init(opts) do
23     opts
24     |> Map.new()
25     |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
26   end
27
28   @impl Plug
29
30   def call(conn, %{operation_id: operation_id, render_error: render_error}) do
31     {spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
32     operation = operation_lookup[operation_id]
33
34     content_type =
35       case Conn.get_req_header(conn, "content-type") do
36         [header_value | _] ->
37           header_value
38           |> String.split(";")
39           |> List.first()
40
41         _ ->
42           "application/json"
43       end
44
45     conn = Conn.put_private(conn, :operation_id, operation_id)
46
47     case cast_and_validate(spec, operation, conn, content_type, strict?()) do
48       {:ok, conn} ->
49         conn
50
51       {:error, reason} ->
52         opts = render_error.init(reason)
53
54         conn
55         |> render_error.call(opts)
56         |> Plug.Conn.halt()
57     end
58   end
59
60   def call(
61         %{
62           private: %{
63             phoenix_controller: controller,
64             phoenix_action: action,
65             open_api_spex: %{spec_module: spec_module}
66           }
67         } = conn,
68         opts
69       ) do
70     {spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
71
72     operation =
73       case operation_lookup[{controller, action}] do
74         nil ->
75           operation_id = controller.open_api_operation(action).operationId
76           operation = operation_lookup[operation_id]
77
78           operation_lookup = Map.put(operation_lookup, {controller, action}, operation)
79
80           OpenApiSpex.Plug.Cache.adapter().put(spec_module, {spec, operation_lookup})
81
82           operation
83
84         operation ->
85           operation
86       end
87
88     if operation.operationId do
89       call(conn, Map.put(opts, :operation_id, operation.operationId))
90     else
91       raise "operationId was not found in action API spec"
92     end
93   end
94
95   def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
96
97   defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
98     OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
99   end
100
101   defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
102     case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
103       {:ok, conn} ->
104         {:ok, conn}
105
106       # Remove unexpected query params and cast/validate again
107       {:error, errors} ->
108         query_params =
109           Enum.reduce(errors, conn.query_params, fn
110             %{reason: :unexpected_field, name: name, path: [name]}, params ->
111               Map.delete(params, name)
112
113             # Filter out empty params
114             %{reason: :invalid_type, path: [name_atom], value: ""}, params ->
115               Map.delete(params, to_string(name_atom))
116
117             %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
118               path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
119               update_in(params, path, &List.delete(&1, value))
120
121             _, params ->
122               params
123           end)
124
125         conn = %Conn{conn | query_params: query_params}
126         OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
127     end
128   end
129
130   defp list_items_to_string(list) do
131     Enum.map(list, fn
132       i when is_atom(i) -> to_string(i)
133       i -> i
134     end)
135   end
136
137   defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
138 end