ec04c05f088853cefc5a8b58561a3c1be58409e4
[akkoma] / lib / pleroma / web / web.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Plug do
6 # Substitute for `call/2` which is defined with `use Pleroma.Web, :plug`
7 @callback perform(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
8 end
9
10 defmodule Pleroma.Web do
11 @moduledoc """
12 A module that keeps using definitions for controllers,
13 views and so on.
14
15 This can be used in your application as:
16
17 use Pleroma.Web, :controller
18 use Pleroma.Web, :view
19
20 The definitions below will be executed for every view,
21 controller, etc, so keep them short and clean, focused
22 on imports, uses and aliases.
23
24 Do NOT define functions inside the quoted expressions
25 below.
26 """
27
28 alias Pleroma.Plugs.EnsureAuthenticatedPlug
29 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
30 alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug
31 alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug
32 alias Pleroma.Plugs.OAuthScopesPlug
33 alias Pleroma.Plugs.PlugHelper
34
35 def controller do
36 quote do
37 use Phoenix.Controller, namespace: Pleroma.Web
38
39 import Plug.Conn
40
41 import Pleroma.Web.Gettext
42 import Pleroma.Web.Router.Helpers
43 import Pleroma.Web.TranslationHelpers
44
45 plug(:set_put_layout)
46
47 defp set_put_layout(conn, _) do
48 put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
49 end
50
51 # Marks plugs intentionally skipped and blocks their execution if present in plugs chain
52 defp skip_plug(conn, plug_modules) do
53 plug_modules
54 |> List.wrap()
55 |> Enum.reduce(
56 conn,
57 fn plug_module, conn ->
58 try do
59 plug_module.skip_plug(conn)
60 rescue
61 UndefinedFunctionError ->
62 raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
63 end
64 end
65 )
66 end
67
68 # Executed just before actual controller action, invokes before-action hooks (callbacks)
69 defp action(conn, params) do
70 with %Plug.Conn{halted: false} <- maybe_perform_public_or_authenticated_check(conn),
71 %Plug.Conn{halted: false} <- maybe_perform_authenticated_check(conn),
72 %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do
73 super(conn, params)
74 end
75 end
76
77 # Ensures instance is public -or- user is authenticated if such check was scheduled
78 defp maybe_perform_public_or_authenticated_check(conn) do
79 if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
80 EnsurePublicOrAuthenticatedPlug.call(conn, %{})
81 else
82 conn
83 end
84 end
85
86 # Ensures user is authenticated if such check was scheduled
87 # Note: runs prior to action even if it was already executed earlier in plug chain
88 # (since OAuthScopesPlug has option of proceeding unauthenticated)
89 defp maybe_perform_authenticated_check(conn) do
90 if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
91 EnsureAuthenticatedPlug.call(conn, %{})
92 else
93 conn
94 end
95 end
96
97 # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
98 defp maybe_halt_on_missing_oauth_scopes_check(conn) do
99 if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
100 not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
101 conn
102 |> render_error(
103 :forbidden,
104 "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
105 )
106 |> halt()
107 else
108 conn
109 end
110 end
111 end
112 end
113
114 def view do
115 quote do
116 use Phoenix.View,
117 root: "lib/pleroma/web/templates",
118 namespace: Pleroma.Web
119
120 # Import convenience functions from controllers
121 import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
122
123 import Pleroma.Web.ErrorHelpers
124 import Pleroma.Web.Gettext
125 import Pleroma.Web.Router.Helpers
126
127 require Logger
128
129 @doc "Same as `render/3` but wrapped in a rescue block"
130 def safe_render(view, template, assigns \\ %{}) do
131 Phoenix.View.render(view, template, assigns)
132 rescue
133 error ->
134 Logger.error(
135 "#{__MODULE__} failed to render #{inspect({view, template})}\n" <>
136 Exception.format(:error, error, __STACKTRACE__)
137 )
138
139 nil
140 end
141
142 @doc """
143 Same as `render_many/4` but wrapped in rescue block.
144 """
145 def safe_render_many(collection, view, template, assigns \\ %{}) do
146 Enum.map(collection, fn resource ->
147 as = Map.get(assigns, :as) || view.__resource__
148 assigns = Map.put(assigns, as, resource)
149 safe_render(view, template, assigns)
150 end)
151 |> Enum.filter(& &1)
152 end
153 end
154 end
155
156 def router do
157 quote do
158 use Phoenix.Router
159 # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
160 import Plug.Conn
161 import Phoenix.Controller
162 end
163 end
164
165 def channel do
166 quote do
167 # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
168 use Phoenix.Channel
169 import Pleroma.Web.Gettext
170 end
171 end
172
173 def plug do
174 quote do
175 @behaviour Pleroma.Web.Plug
176 @behaviour Plug
177
178 @doc """
179 Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
180 """
181 def skip_plug(conn) do
182 PlugHelper.append_to_private_list(
183 conn,
184 PlugHelper.skipped_plugs_list_id(),
185 __MODULE__
186 )
187 end
188
189 @impl Plug
190 @doc """
191 If marked as skipped, returns `conn`, otherwise calls `perform/2`.
192 Note: multiple invocations of the same plug (with different or same options) are allowed.
193 """
194 def call(%Plug.Conn{} = conn, options) do
195 if PlugHelper.plug_skipped?(conn, __MODULE__) do
196 conn
197 else
198 conn =
199 PlugHelper.append_to_private_list(
200 conn,
201 PlugHelper.called_plugs_list_id(),
202 __MODULE__
203 )
204
205 apply(__MODULE__, :perform, [conn, options])
206 end
207 end
208 end
209 end
210
211 @doc """
212 When used, dispatch to the appropriate controller/view/etc.
213 """
214 defmacro __using__(which) when is_atom(which) do
215 apply(__MODULE__, which, [])
216 end
217
218 def base_url do
219 Pleroma.Web.Endpoint.url()
220 end
221 end