summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/api_spec/cast_and_validate.ex
blob: d23a7dcb6c4fd8b57fdd5cfc6f1d1c88e89290dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# Pleroma: A lightweight social networking server
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.CastAndValidate do
  @moduledoc """
  This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
  (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
  The main difference is ignoring unexpected query params instead of throwing
  an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
  to disable this behavior. Also, the default rendering error module
  is `Pleroma.Web.ApiSpec.RenderError`.
  """

  @behaviour Plug

  alias OpenApiSpex.Plug.PutApiSpec
  alias Plug.Conn

  @impl Plug
  def init(opts) do
    opts
    |> Map.new()
    |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
  end

  @impl Plug

  def call(conn, %{operation_id: operation_id, render_error: render_error}) do
    {spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
    operation = operation_lookup[operation_id]

    content_type =
      case Conn.get_req_header(conn, "content-type") do
        [header_value | _] ->
          header_value
          |> String.split(";")
          |> List.first()

        _ ->
          "application/json"
      end

    conn = Conn.put_private(conn, :operation_id, operation_id)

    case cast_and_validate(spec, operation, conn, content_type, strict?()) do
      {:ok, conn} ->
        conn

      {:error, reason} ->
        opts = render_error.init(reason)

        conn
        |> render_error.call(opts)
        |> Plug.Conn.halt()
    end
  end

  def call(
        %{
          private: %{
            phoenix_controller: controller,
            phoenix_action: action,
            open_api_spex: %{spec_module: spec_module}
          }
        } = conn,
        opts
      ) do
    {spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)

    operation =
      case operation_lookup[{controller, action}] do
        nil ->
          operation_id = controller.open_api_operation(action).operationId
          operation = operation_lookup[operation_id]

          operation_lookup = Map.put(operation_lookup, {controller, action}, operation)

          OpenApiSpex.Plug.Cache.adapter().put(spec_module, {spec, operation_lookup})

          operation

        operation ->
          operation
      end

    if operation.operationId do
      call(conn, Map.put(opts, :operation_id, operation.operationId))
    else
      raise "operationId was not found in action API spec"
    end
  end

  def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)

  defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
    OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
  end

  defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
    case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
      {:ok, conn} ->
        {:ok, conn}

      # Remove unexpected query params and cast/validate again
      {:error, errors} ->
        query_params =
          Enum.reduce(errors, conn.query_params, fn
            %{reason: :unexpected_field, name: name, path: [name]}, params ->
              Map.delete(params, name)

            # Filter out empty params
            %{reason: :invalid_type, path: [name_atom], value: ""}, params ->
              Map.delete(params, to_string(name_atom))

            %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
              path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
              update_in(params, path, &List.delete(&1, value))

            _, params ->
              params
          end)

        conn = %Conn{conn | query_params: query_params}
        OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
    end
  end

  defp list_items_to_string(list) do
    Enum.map(list, fn
      i when is_atom(i) -> to_string(i)
      i -> i
    end)
  end

  defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
end