Merge pull request 'Fix preferred frontends' (#7911) from phnt/pleroma:fix-preferred-frontend into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7911
This commit is contained in:
Phantasm 2026-07-20 15:29:02 +00:00
commit 0a076443ad
8 changed files with 128 additions and 74 deletions

View file

@ -0,0 +1 @@
Fix preferred frontend not working due to InstanceStatic plug prematurely returning HTTP 404

View file

@ -139,6 +139,8 @@ defmodule Pleroma.Web.Endpoint do
from: {:pleroma, "priv/static/adminfe/"} from: {:pleroma, "priv/static/adminfe/"}
) )
plug(Pleroma.Web.Plugs.StaticNotFoundPlug)
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.
if code_reloading? do if code_reloading? do

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do
@moduledoc "Ensures Host header matches instance" @moduledoc "Ensures Host header matches instance"
@behaviour Plug
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants require Pleroma.Constants
import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1] import Plug.Conn, only: [put_resp_header: 3]
@moduledoc """ @moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration. This is a shim to call `Plug.Static` but with runtime `from` configuration.
@ -51,37 +51,10 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
|> Map.put(:from, from) |> Map.put(:from, from)
|> Map.put(:content_types, false) |> Map.put(:content_types, false)
conn = conn = set_content_type(conn, conn.request_path)
conn
|> set_content_type(conn.request_path)
|> Plug.Static.call(opts)
if conn.halted do # Call Plug.Static with our sanitized content-type
conn Plug.Static.call(conn, opts)
else
path = String.trim_leading(conn.request_path, "/")
if not File.exists?(file_path(path)) do
conn
|> put_status(:not_found)
|> send_404()
|> halt()
else
conn
end
end
end
defp send_404(conn) do
if String.ends_with?(String.downcase(conn.request_path), ".json") do
conn
|> put_resp_header("content-type", "application/json")
|> send_resp(404, Jason.encode!(%{error: "not found"}))
else
conn
|> put_resp_header("content-type", "text/plain")
|> send_resp(404, "Not found")
end
end end
defp set_content_type(conn, "/emoji/" <> filepath) do defp set_content_type(conn, "/emoji/" <> filepath) do

View file

@ -0,0 +1,38 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.StaticNotFoundPlug do
@moduledoc """
A small plug that enforces returning 404 on static files when they don't exist.
"""
@behaviour Plug
require Pleroma.Constants
import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1]
def init(opts), do: opts
for only <- Pleroma.Constants.static_only_files() do
def call(%{request_path: "/" <> unquote(only) <> _} = conn, _opts) do
send_404(conn)
end
end
def call(conn, _) do
conn
end
defp send_404(conn) do
if String.ends_with?(String.downcase(conn.request_path), ".json") do
conn
|> put_resp_header("content-type", "application/json")
|> send_resp(404, Jason.encode!(%{error: "not found"}))
else
conn
|> put_resp_header("content-type", "text/plain")
|> send_resp(404, "Not found")
end
|> halt()
end
end

View file

@ -122,16 +122,27 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :pickable], ["#{name}/#{ref}"]) clear_config([:frontends, :pickable], ["#{name}/#{ref}"])
path = "#{@dir}/frontends/#{name}/#{ref}" path = "#{@dir}/frontends/#{name}/#{ref}"
path2 = "#{@dir}/frontends/#{name}/#{ref}/packs/html"
Pleroma.Backports.mkdir_p!(path) Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug") File.write!("#{path}/index.html", "from frontend plug")
Pleroma.Backports.mkdir_p!(path2)
File.write!("#{path2}/custom.html", "from frontend plug")
index = index =
conn conn
|> put_req_cookie("preferred_frontend", "#{name}/#{ref}") |> put_req_cookie("preferred_frontend", "#{name}/#{ref}")
|> get("/") |> get("/")
assert html_response(index, 200) == "from frontend plug" assert html_response(index, 200) == "from frontend plug"
custom =
conn
|> put_req_cookie("preferred_frontend", "#{name}/#{ref}")
|> get("/packs/html/custom.html")
assert html_response(custom, 200) == "from frontend plug"
end end
test "only returns content from pickable frontends", %{conn: conn} do test "only returns content from pickable frontends", %{conn: conn} do

View file

@ -61,6 +61,28 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>") File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>")
index = get(build_conn(), "/static/kaniini.html") index = get(build_conn(), "/static/kaniini.html")
assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>" assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>"
File.write!(@dir <> "/static/kaniini the bnuuy.html", "<h1>rabbit hugs as a service</h1>")
index = get(build_conn(), "/static/kaniini%20the%20bnuuy.html")
assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>"
end
test "overrides /static/styles files with spaces in URL" do
Pleroma.Backports.mkdir_p!(@dir <> "/static/styles")
File.write(@dir <> "/static/styles/Redmond DX.iss", "overridden")
index = get(build_conn(), "/static/styles/Redmond%20DX.iss")
assert index.status == 200
assert index.resp_body == File.read!(@dir <> "/static/styles/Redmond DX.iss")
assert index.halted == true
end
test "serves fallback /static/styles files with spaces in URL" do
index = get(build_conn(), "/static/styles/Redmond%20DX.iss")
assert index.status == 200
assert index.resp_body == File.read!("priv/static/static/styles/Redmond DX.iss")
assert index.halted == true
end end
test "does not sanitize dangerous files in general, as there can be html and javascript files legitimately in this folder" do test "does not sanitize dangerous files in general, as there can be html and javascript files legitimately in this folder" do
@ -137,47 +159,4 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
# It should be preserved because "image" is in the allowed_mime_types list # It should be preserved because "image" is in the allowed_mime_types list
assert content_type == "image/jpeg" assert content_type == "image/jpeg"
end end
describe "404s for missing files in static-only paths" do
test "returns 404 for non-existent static-only JSON files" do
conn = get(build_conn(), "/static/non-existent.json")
assert conn.status == 404
assert ["application/json"] = get_resp_header(conn, "content-type")
assert Jason.decode!(conn.resp_body) == %{"error" => "not found"}
end
test "returns 404 for non-existent static-only non-JSON files" do
conn = get(build_conn(), "/static/non-existent.txt")
assert conn.status == 404
assert conn.resp_body == "Not found"
assert ["text/plain"] = get_resp_header(conn, "content-type")
end
test "returns 404 for non-existent .css files" do
conn = get(build_conn(), "/static/non-existent.css")
assert conn.status == 404
assert conn.resp_body == "Not found"
# Verifies that we forced text/plain for the error body, even though the path was .css
assert ["text/plain"] = get_resp_header(conn, "content-type")
end
test "returns 404 for non-existent files without an extension" do
conn = get(build_conn(), "/static/non-existent")
assert conn.status == 404
assert conn.resp_body == "Not found"
assert ["text/plain"] = get_resp_header(conn, "content-type")
end
test "returns 200 (falls through to SPA) for non-static-only paths" do
# /some-route is NOT in static_only_files, so it should still fall through to the SPA.
conn = get(build_conn(), "/some-route")
assert conn.status == 200
assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type")
end
end
end end

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.StaticNotFoundPlugTest do
use Pleroma.Web.ConnCase
describe "404s for missing files in static-only paths" do
test "returns 404 for non-existent static-only JSON files" do
conn = get(build_conn(), "/static/non-existent.json")
assert conn.status == 404
assert ["application/json"] = get_resp_header(conn, "content-type")
assert Jason.decode!(conn.resp_body) == %{"error" => "not found"}
end
test "returns 404 for non-existent static-only non-JSON files" do
conn = get(build_conn(), "/static/non-existent.txt")
assert conn.status == 404
assert conn.resp_body == "Not found"
assert ["text/plain"] = get_resp_header(conn, "content-type")
end
test "returns 404 for non-existent .css files" do
conn = get(build_conn(), "/static/non-existent.css")
assert conn.status == 404
assert conn.resp_body == "Not found"
# Verifies that we forced text/plain for the error body, even though the path was .css
assert ["text/plain"] = get_resp_header(conn, "content-type")
end
test "returns 404 for non-existent files without an extension" do
conn = get(build_conn(), "/static/non-existent")
assert conn.status == 404
assert conn.resp_body == "Not found"
assert ["text/plain"] = get_resp_header(conn, "content-type")
end
test "returns 200 (falls through to SPA) for non-static-only paths" do
# /some-route is NOT in static_only_files, so it should still fall through to the SPA.
conn = get(build_conn(), "/some-route")
assert conn.status == 200
assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type")
end
end
end