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