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