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