First
[anni] / lib / pleroma / web / plugs / cache.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.Plugs.Cache do
6   @moduledoc """
7   Caches successful GET responses.
8
9   To enable the cache add the plug to a router pipeline or controller:
10
11       plug(Pleroma.Web.Plugs.Cache)
12
13   ## Configuration
14
15   To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
16
17       plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true])
18
19   Available options:
20
21   - `ttl`:  An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
22   - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
23   - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
24
25   Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
26
27       def index(conn, _params) do
28         ttl = 60_000 # one minute
29
30         conn
31         |> assign(:cache_ttl, ttl)
32         |> render("index.html")
33       end
34
35   """
36
37   import Phoenix.Controller, only: [current_path: 1, json: 2]
38   import Plug.Conn
39
40   @behaviour Plug
41
42   @defaults %{ttl: nil, query_params: true}
43
44   @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
45
46   @impl true
47   def init([]), do: @defaults
48
49   def init(opts) do
50     opts = Map.new(opts)
51     Map.merge(@defaults, opts)
52   end
53
54   @impl true
55   def call(%{method: "GET"} = conn, opts) do
56     key = cache_key(conn, opts)
57
58     case @cachex.get(:web_resp_cache, key) do
59       {:ok, nil} ->
60         cache_resp(conn, opts)
61
62       {:ok, {content_type, body, tracking_fun_data}} ->
63         conn = opts.tracking_fun.(conn, tracking_fun_data)
64
65         send_cached(conn, {content_type, body})
66
67       {:ok, record} ->
68         send_cached(conn, record)
69
70       {atom, message} when atom in [:ignore, :error] ->
71         render_error(conn, message)
72     end
73   end
74
75   def call(conn, _), do: conn
76
77   # full path including query params
78   defp cache_key(conn, %{query_params: true}), do: current_path(conn)
79
80   # request path without query params
81   defp cache_key(conn, %{query_params: false}), do: conn.request_path
82
83   # request path with specific query params
84   defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
85     query_string =
86       conn.params
87       |> Map.take(query_params)
88       |> URI.encode_query()
89
90     conn.request_path <> "?" <> query_string
91   end
92
93   defp cache_resp(conn, opts) do
94     register_before_send(conn, fn
95       %{status: 200, resp_body: body} = conn ->
96         ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
97         key = cache_key(conn, opts)
98         content_type = content_type(conn)
99
100         should_cache = not Map.get(conn.assigns, :skip_cache, false)
101
102         conn =
103           unless opts[:tracking_fun] do
104             if should_cache do
105               @cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
106             end
107
108             conn
109           else
110             tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
111
112             if should_cache do
113               @cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
114             end
115
116             opts.tracking_fun.(conn, tracking_fun_data)
117           end
118
119         put_resp_header(conn, "x-cache", "MISS from Pleroma")
120
121       conn ->
122         conn
123     end)
124   end
125
126   defp content_type(conn) do
127     conn
128     |> Plug.Conn.get_resp_header("content-type")
129     |> hd()
130   end
131
132   defp send_cached(conn, {content_type, body}) do
133     conn
134     |> put_resp_content_type(content_type, nil)
135     |> put_resp_header("x-cache", "HIT from Pleroma")
136     |> send_resp(:ok, body)
137     |> halt()
138   end
139
140   defp render_error(conn, message) do
141     conn
142     |> put_status(:internal_server_error)
143     |> json(%{error: message})
144     |> halt()
145   end
146 end