metadata providers consistency
[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 %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn),
71 %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
72 %{halted: false} = conn <- maybe_perform_authenticated_check(conn),
73 %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
74 super(conn, params)
75 end
76 end
77
78 # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored
79 # (neither performed nor explicitly skipped)
80 defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
81 if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
82 not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
83 OAuthScopesPlug.drop_auth_info(conn)
84 else
85 conn
86 end
87 end
88
89 # Ensures instance is public -or- user is authenticated if such check was scheduled
90 defp maybe_perform_public_or_authenticated_check(conn) do
91 if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
92 EnsurePublicOrAuthenticatedPlug.call(conn, %{})
93 else
94 conn
95 end
96 end
97
98 # Ensures user is authenticated if such check was scheduled
99 # Note: runs prior to action even if it was already executed earlier in plug chain
100 # (since OAuthScopesPlug has option of proceeding unauthenticated)
101 defp maybe_perform_authenticated_check(conn) do
102 if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
103 EnsureAuthenticatedPlug.call(conn, %{})
104 else
105 conn
106 end
107 end
108
109 # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
110 defp maybe_halt_on_missing_oauth_scopes_check(conn) do
111 if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
112 not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
113 conn
114 |> render_error(
115 :forbidden,
116 "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
117 )
118 |> halt()
119 else
120 conn
121 end
122 end
123 end
124 end
125
126 def view do
127 quote do
128 use Phoenix.View,
129 root: "lib/pleroma/web/templates",
130 namespace: Pleroma.Web
131
132 # Import convenience functions from controllers
133 import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
134
135 import Pleroma.Web.ErrorHelpers
136 import Pleroma.Web.Gettext
137 import Pleroma.Web.Router.Helpers
138
139 require Logger
140
141 @doc "Same as `render/3` but wrapped in a rescue block"
142 def safe_render(view, template, assigns \\ %{}) do
143 Phoenix.View.render(view, template, assigns)
144 rescue
145 error ->
146 Logger.error(
147 "#{__MODULE__} failed to render #{inspect({view, template})}\n" <>
148 Exception.format(:error, error, __STACKTRACE__)
149 )
150
151 nil
152 end
153
154 @doc """
155 Same as `render_many/4` but wrapped in rescue block.
156 """
157 def safe_render_many(collection, view, template, assigns \\ %{}) do
158 Enum.map(collection, fn resource ->
159 as = Map.get(assigns, :as) || view.__resource__
160 assigns = Map.put(assigns, as, resource)
161 safe_render(view, template, assigns)
162 end)
163 |> Enum.filter(& &1)
164 end
165 end
166 end
167
168 def router do
169 quote do
170 use Phoenix.Router
171 # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
172 import Plug.Conn
173 import Phoenix.Controller
174 end
175 end
176
177 def channel do
178 quote do
179 # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
180 use Phoenix.Channel
181 import Pleroma.Web.Gettext
182 end
183 end
184
185 def plug do
186 quote do
187 @behaviour Pleroma.Web.Plug
188 @behaviour Plug
189
190 @doc """
191 Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
192 """
193 def skip_plug(conn) do
194 PlugHelper.append_to_private_list(
195 conn,
196 PlugHelper.skipped_plugs_list_id(),
197 __MODULE__
198 )
199 end
200
201 @impl Plug
202 @doc """
203 Before-plug hook that
204 * ensures the plug is not skipped
205 * processes `:if_func` / `:unless_func` functional pre-run conditions
206 * adds plug to the list of called plugs and calls `perform/2` if checks are passed
207
208 Note: multiple invocations of the same plug (with different or same options) are allowed.
209 """
210 def call(%Plug.Conn{} = conn, options) do
211 if PlugHelper.plug_skipped?(conn, __MODULE__) ||
212 (options[:if_func] && !options[:if_func].(conn)) ||
213 (options[:unless_func] && options[:unless_func].(conn)) do
214 conn
215 else
216 conn =
217 PlugHelper.append_to_private_list(
218 conn,
219 PlugHelper.called_plugs_list_id(),
220 __MODULE__
221 )
222
223 apply(__MODULE__, :perform, [conn, options])
224 end
225 end
226 end
227 end
228
229 @doc """
230 When used, dispatch to the appropriate controller/view/etc.
231 """
232 defmacro __using__(which) when is_atom(which) do
233 apply(__MODULE__, which, [])
234 end
235
236 def base_url do
237 Pleroma.Web.Endpoint.url()
238 end
239 end