diff --git a/changelog.d/cache-control-immutable.add b/changelog.d/cache-control-immutable.add
new file mode 100644
index 000000000..516db67bf
--- /dev/null
+++ b/changelog.d/cache-control-immutable.add
@@ -0,0 +1 @@
+Add immutable tag on cache-control header for several endpoints that's serving the same exact things.
\ No newline at end of file
diff --git a/changelog.d/poll-voters-count.fix b/changelog.d/poll-voters-count.fix
new file mode 100644
index 000000000..2dbc81b5d
--- /dev/null
+++ b/changelog.d/poll-voters-count.fix
@@ -0,0 +1 @@
+Federate `votersCount` correctly
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index f78b84099..178dd6094 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Constants do
const(static_only_files,
do:
- ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
+ ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js schemas doc embed.js embed.css)
)
const(status_updatable_fields,
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index d0cb16b79..f1e07a257 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -374,10 +374,18 @@ defmodule Pleroma.Object do
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
+ voters_count =
+ if Map.has_key?(object.data, "votersCount") do
+ object.data["votersCount"] + 1
+ else
+ length(voters)
+ end
+
data =
object.data
|> Map.put(key, options)
|> Map.put("voters", voters)
+ |> Map.put("votersCount", voters_count)
object
|> Object.change(%{data: data})
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index bb55a4984..c7ee47c6e 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.ReverseProxy do
@keep_resp_headers @resp_cache_headers ++
~w(content-length content-type content-disposition content-encoding) ++
~w(content-range accept-ranges vary)
- @default_cache_control_header "public, max-age=1209600"
+ @default_cache_control_header "public, max-age=1209600, immutable"
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
index 21940f4f1..065c75910 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -28,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
end
field(:closed, ObjectValidators.DateTime)
+ field(:votersCount, :integer)
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
field(:nonAnonymous, :boolean)
embeds_many(:anyOf, QuestionOptionsValidator)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 6af5ee89d..4421da26c 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -783,6 +783,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def set_replies(obj_data), do: obj_data
+ defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do
+ Map.merge(obj, %{"votersCount" => length(voters)})
+ end
+
+ defp set_voters_count(obj), do: obj
+
# Prepares and sanitizes the object for federation.
def prepare_object(object) do
object
@@ -795,6 +801,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> set_quote_url
|> set_replies
+ |> set_voters_count
|> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index bab3c9fd0..81a9d3a09 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -46,8 +46,9 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
- @static_cache_control "public, max-age=1209600"
+ @static_cache_control "public, max-age=1209600, immutable"
@static_cache_disabled "public, no-cache"
+ @favicon_cache_control "public, max=age=86400, immutable" # cache for a day
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
@@ -64,6 +65,15 @@ defmodule Pleroma.Web.Endpoint do
}
)
+ plug(Pleroma.Web.Plugs.FaviconPlug,
+ at: "/",
+ only: ["favicon.png"],
+ cache_control_for_etags: @favicon_cache_control,
+ headers: %{
+ "cache-control" => @favicon_cache_control
+ }
+ )
+
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 1e3c9f36d..3b4271227 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -75,6 +75,8 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
length(voters)
end
+ defp voters_count(%{data: %{"votersCount" => voters}}), do: voters
+
defp voters_count(_), do: 0
defp voted_and_own_votes(%{object: object} = params, options) do
diff --git a/lib/pleroma/web/plugs/favicon_plug.ex b/lib/pleroma/web/plugs/favicon_plug.ex
new file mode 100644
index 000000000..e2e1f1adb
--- /dev/null
+++ b/lib/pleroma/web/plugs/favicon_plug.ex
@@ -0,0 +1,71 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2026 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.FaviconPlug do
+ @behaviour Plug
+
+ @moduledoc """
+ This is a shim to call `Plug.Static` but with runtime `from` configuration for instance favicon.
+
+ Serves default or custom favicon.png with cacheable cache-control.
+ """
+
+ import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1]
+
+ require Logger
+
+ def init(opts) do
+ opts
+ |> Keyword.put(:from, "__unconfigured_favicon_static_plug")
+ |> Plug.Static.init()
+ end
+
+ def call(%{request_path: "/favicon.png"} = conn, opts) do
+ case find_favicon_dir() do
+ {:ok, dir} ->
+ call_static(conn, opts, dir)
+
+ :error ->
+ # Favicon should always be available and this should never occur.
+ # If it does, halt the pipeline before having unintended side-effects.
+ Logger.error("No favicon.png found! Is the default favicon deleted?")
+
+ conn
+ |> send_resp(404, "Not found")
+ |> halt()
+ end
+ end
+
+ def call(conn, _) do
+ conn
+ end
+
+ defp find_favicon_dir do
+ instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ instance_path = Path.join(instance_dir, "favicon.png")
+
+ priv_dir = Application.app_dir(:pleroma, "priv/static")
+ priv_path = Path.join(priv_dir, "favicon.png")
+
+ cond do
+ File.exists?(instance_path) -> {:ok, instance_dir}
+ File.exists?(priv_path) -> {:ok, priv_dir}
+ true -> :error
+ end
+ end
+
+ defp call_static(conn, opts, from) do
+ opts =
+ opts
+ |> Map.put(:from, from)
+ |> Map.put(:content_types, false)
+
+ conn = set_content_type(conn)
+ Plug.Static.call(conn, opts)
+ end
+
+ defp set_content_type(conn) do
+ put_resp_header(conn, "content-type", "image/png")
+ end
+end
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index abacf965b..5a3ea12aa 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
# no slashes
@path "media"
- @default_cache_control_header "public, max-age=1209600"
+ @default_cache_control_header "public, max-age=1209600, immutable"
def init(_opts) do
static_plug_opts =
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
index 8dbe9c6bf..ec4470379 100644
--- a/test/pleroma/reverse_proxy_test.exs
+++ b/test/pleroma/reverse_proxy_test.exs
@@ -294,7 +294,7 @@ defmodule Pleroma.ReverseProxyTest do
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/cache")
- assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers
+ assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers
end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs
index d31070546..2a4e78cf0 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs
@@ -170,4 +170,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do
assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
end
+
+ test "it displays voters count for a poll" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "???",
+ poll: %{expires_in: 10, options: ["yes", "no"]}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+ {:ok, _, _} = CommonAPI.vote(object, other_user, [1])
+
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
+
+ refute Map.has_key?(modified["object"], "voters")
+ assert modified["object"]["votersCount"] == 1
+ end
end
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
index 6de001421..16281393d 100644
--- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -167,7 +167,14 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
} = PollView.render("show.json", %{object: object})
end
- test "that poll is non anonymous" do
+ test "displays correct voters count" do
+ object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 14
+ end
+
+ test "detects that poll is non anonymous" do
object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
result = PollView.render("show.json", %{object: object})
diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs
new file mode 100644
index 000000000..520501250
--- /dev/null
+++ b/test/pleroma/web/plugs/favicon_plug_test.exs
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2026 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.FaviconPlugTest do
+ use Pleroma.Web.ConnCase
+
+ @dir "test/tmp/favicon_static"
+
+ setup do
+ Pleroma.Backports.mkdir_p!(@dir)
+
+ on_exit(fn -> File.rm_rf!(@dir) end)
+ end
+
+ describe "default favicon" do
+ test "returns favicon", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ body_size = byte_size(conn.resp_body)
+
+ assert conn.status == 200
+ assert body_size == 1583
+ assert response_content_type(conn, :png)
+ end
+
+ test "returns correct cache-control", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ cache = get_resp_header(conn, "cache-control")
+
+ assert conn.status == 200
+ assert cache == ["public, max=age=86400, immutable"]
+ end
+ end
+
+ describe "custom favicon" do
+ setup do
+ favicon_path = Path.join(@dir, "favicon.png")
+ donor_image = "test/fixtures/image.png"
+
+ File.cp!(donor_image, favicon_path)
+ clear_config([:instance, :static_dir], @dir)
+
+ on_exit(fn -> File.rm!(favicon_path) end)
+ end
+
+ test "returns favicon", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ body_size = byte_size(conn.resp_body)
+
+ assert conn.status == 200
+ assert body_size == 104_426
+ assert response_content_type(conn, :png)
+ end
+
+ test "returns correct cache-control", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ cache = get_resp_header(conn, "cache-control")
+
+ assert conn.status == 200
+ assert cache == ["public, max=age=86400, immutable"]
+ end
+ end
+end