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