commit: 2620b89cb5302cb88816d5c8001b540864996680 parent f1d588fd6a04945aa45bd4d1711d142de2e67c43 Author: lain <lain@soykaf.club> Date: Wed, 7 Jan 2026 17:03:18 +0000 Merge branch 'issue-3389-emoji-react-encode' into 'develop' Issue 3389 emoji react encode See merge request pleroma/pleroma!4417Diffstat:
15 files changed, 139 insertions(+), 40 deletions(-)diff --git a/changelog.d/emoji-reaction-url-escape.fix b/changelog.d/emoji-reaction-url-escape.fix@@ -0,0 +1 @@ +Encode custom emoji URLs in EmojiReact activity tags.diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex@@ -11,6 +11,8 @@ defmodule Pleroma.Emoji do alias Pleroma.Emoji.Combinations alias Pleroma.Emoji.Loader + alias Pleroma.Utils.URIEncoding + alias Pleroma.Web.Endpoint require Logger @@ -189,6 +191,34 @@ defmodule Pleroma.Emoji do def emoji_url(_), do: nil + @spec local_url(String.t() | nil) :: String.t() | nil + def local_url(nil), do: nil + + def local_url("http" <> _ = url) do + URIEncoding.encode_url(url) + end + + def local_url("/" <> _ = path) do + path = URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + Endpoint.url() <> path + end + + def local_url(path) when is_binary(path) do + local_url("/" <> path) + end + + def build_emoji_tag({name, url}) do + url = URIEncoding.encode_url(url) + + %{ + "icon" => %{"url" => "#{url}", "type" => "Image"}, + "name" => ":" <> name <> ":", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z", + "id" => url + } + end + def emoji_name_with_instance(name, url) do url = url |> URI.parse() |> Map.get(:host) "#{name}@#{url}"diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex@@ -5,7 +5,6 @@ defmodule Pleroma.Emoji.Formatter do alias Pleroma.Emoji alias Pleroma.HTML - alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy def emojify(text) do @@ -44,7 +43,7 @@ defmodule Pleroma.Emoji.Formatter do Emoji.get_all() |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end) |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc -> - Map.put(acc, name, to_string(URI.merge(Endpoint.url(), file))) + Map.put(acc, name, Emoji.local_url(file)) end) enddiff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex@@ -17,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI.ActivityDraft - alias Pleroma.Web.Endpoint require Pleroma.Constants @@ -64,15 +63,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do defp add_emoji_content(data, emoji, url) do tag = [ - %{ - "id" => url, - "type" => "Emoji", - "name" => Emoji.maybe_quote(emoji), - "icon" => %{ - "type" => "Image", - "url" => url - } - } + Emoji.build_emoji_tag({Emoji.maybe_strip_name(emoji), url}) ] data @@ -113,7 +104,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do defp local_custom_emoji_react(data, emoji) do with %{file: path} = emojo <- Emoji.get(emoji) do - url = "#{Endpoint.url()}#{path}" + url = Emoji.local_url(path) add_emoji_content(data, emojo.code, url) else _ -> {:error, "Emoji does not exist"}diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do @behaviour Pleroma.Web.ActivityPub.Transmogrifier.API alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Emoji alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment @@ -1005,32 +1006,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def take_emoji_tags(%User{emoji: emoji}) do emoji |> Map.to_list() - |> Enum.map(&build_emoji_tag/1) + |> Enum.map(&Emoji.build_emoji_tag/1) end # TODO: we should probably send mtime instead of unix epoch time for updated def add_emoji_tags(%{"emoji" => emoji} = object) do tags = object["tag"] || [] - out = Enum.map(emoji, &build_emoji_tag/1) + out = Enum.map(emoji, &Emoji.build_emoji_tag/1) Map.put(object, "tag", tags ++ out) end def add_emoji_tags(object), do: object - def build_emoji_tag({name, url}) do - url = URI.encode(url) - - %{ - "icon" => %{"url" => "#{url}", "type" => "Image"}, - "name" => ":" <> name <> ":", - "type" => "Emoji", - "updated" => "1970-01-01T00:00:00Z", - "id" => url - } - end - def set_conversation(object) do Map.put(object, "conversation", object["context"]) enddiff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex@@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do alias Pleroma.User alias Pleroma.UserNote alias Pleroma.UserRelationship + alias Pleroma.Utils.URIEncoding alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MediaProxy @@ -238,7 +239,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do emojis = Enum.map(user.emoji, fn {shortcode, raw_url} -> - url = MediaProxy.url(raw_url) + url = + raw_url + |> encode_emoji_url() + |> MediaProxy.url() %{ shortcode: shortcode, @@ -511,4 +515,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do # See https://git.pleroma.social/pleroma/pleroma-meta/-/issues/14 user.actor_type == "Service" || user.actor_type == "Group" end + + defp encode_emoji_url(nil), do: nil + defp encode_emoji_url("http" <> _ = url), do: URIEncoding.encode_url(url) + + defp encode_emoji_url("/" <> _ = path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + + defp encode_emoji_url(path) when is_binary(path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) enddiff --git a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex@@ -6,14 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiView do use Pleroma.Web, :view alias Pleroma.Emoji - alias Pleroma.Web.Endpoint def render("index.json", %{custom_emojis: custom_emojis}) do render_many(custom_emojis, __MODULE__, "show.json") end def render("show.json", %{custom_emoji: {shortcode, %Emoji{file: relative_url, tags: tags}}}) do - url = Endpoint.url() |> URI.merge(relative_url) |> to_string() + url = Emoji.local_url(relative_url) %{ "shortcode" => shortcode,diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex@@ -7,7 +7,6 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do alias Pleroma.BookmarkFolder alias Pleroma.Emoji - alias Pleroma.Web.Endpoint def render("show.json", %{folder: %BookmarkFolder{} = folder}) do %{ @@ -33,7 +32,7 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do emoji = Emoji.get(emoji) if emoji != nil do - Endpoint.url() |> URI.merge(emoji.file) |> to_string() + Emoji.local_url(emoji.file) else nil enddiff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex@@ -12,6 +12,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Emoji alias Pleroma.Healthcheck alias Pleroma.User + alias Pleroma.Utils.URIEncoding alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.CommonAPI @@ -180,12 +181,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do def emoji(conn, _params) do emoji = Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> + file = encode_emoji_url(file) Map.put(acc, code, %{image_url: file, tags: tags}) end) json(conn, emoji) end + defp encode_emoji_url(nil), do: nil + defp encode_emoji_url("http" <> _ = url), do: URIEncoding.encode_url(url) + + defp encode_emoji_url("/" <> _ = path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + + defp encode_emoji_url(path) when is_binary(path), + do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true) + def update_notification_settings(%{assigns: %{user: user}} = conn, params) do with {:ok, _} <- User.update_notification_settings(user, params) do json(conn, %{status: "success"})diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_tag_building_test.exs@@ -1,14 +1,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do use Pleroma.DataCase, async: true - alias Pleroma.Web.ActivityPub.Transmogrifier - test "it encodes the id to be a valid url" do name = "hanapog" url = "https://misskey.local.live/emojis/hana pog.png" - tag = Transmogrifier.build_emoji_tag({name, url}) + tag = Pleroma.Emoji.build_emoji_tag({name, url}) assert tag["id"] == "https://misskey.local.live/emojis/hana%20pog.png" end + + test "it does not double-encode already encoded urls" do + name = "hanapog" + url = "https://misskey.local.live/emojis/hana%20pog.png" + + tag = Pleroma.Emoji.build_emoji_tag({name, url}) + + assert tag["id"] == url + end + + test "it encodes disallowed path characters" do + name = "hanapog" + url = "https://example.com/emojis/hana[pog].png" + + tag = Pleroma.Emoji.build_emoji_tag({name, url}) + + assert tag["id"] == "https://example.com/emojis/hana%5Bpog%5D.png" + end + + test "local_url does not decode percent in filenames" do + url = Pleroma.Emoji.local_url("/emoji/hana%20pog.png") + + assert url == Pleroma.Web.Endpoint.url() <> "/emoji/hana%2520pog.png" + + tag = Pleroma.Emoji.build_emoji_tag({"hanapog", url}) + + assert tag["id"] == url + end + + test "local_url encodes question marks in filenames" do + url = Pleroma.Emoji.local_url("/emoji/file?name.png") + + assert url == Pleroma.Web.Endpoint.url() <> "/emoji/file%3Fname.png" + end enddiff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs@@ -759,6 +759,22 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do ) end + test "EmojiReact custom emoji urls are URI encoded" do + user = insert(:user, local: true) + note_activity = insert(:note_activity) + + {:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:") + {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data) + + assert length(data["tag"]) == 1 + + tag = List.first(data["tag"]) + url = tag["icon"]["url"] + + assert url == "http://localhost:4001/emoji/dino%20walking.gif" + assert tag["id"] == "http://localhost:4001/emoji/dino%20walking.gif" + end + test "it prepares a quote post" do user = insert(:user)diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs@@ -105,6 +105,25 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end + test "encodes emoji urls in the emojis field" do + user = + insert(:user, + name: ":brackets: :percent:", + emoji: %{ + "brackets" => "/emoji/hana[pog].png", + "percent" => "/emoji/hana%20pog.png" + } + ) + + %{emojis: emojis} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + emoji_urls = Map.new(emojis, &{&1.shortcode, &1.url}) + + assert emoji_urls["brackets"] == "/emoji/hana%5Bpog%5D.png" + assert emoji_urls["percent"] == "/emoji/hana%2520pog.png" + end + describe "roles and privileges" do setup do clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator])diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs@@ -219,7 +219,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do data: %{ "reactions" => [ ["👍", [user.ap_id], nil], - ["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino walking.gif"] + ["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino%20walking.gif"] ] } ) @@ -243,7 +243,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do account: AccountView.render("show.json", %{user: other_user, for: user}), status: StatusView.render("show.json", %{activity: activity, for: user}), created_at: Utils.to_masto_date(notification.inserted_at), - emoji_url: "http://localhost:4001/emoji/dino walking.gif" + emoji_url: "http://localhost:4001/emoji/dino%20walking.gif" } test_notifications_rendering([notification], user, [expected])diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs@@ -54,7 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do count: 2, me: false, name: "dinosaur", - url: "http://localhost:4001/emoji/dino walking.gif", + url: "http://localhost:4001/emoji/dino%20walking.gif", account_ids: [other_user.id, user.id] }, %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} @@ -70,7 +70,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do count: 2, me: true, name: "dinosaur", - url: "http://localhost:4001/emoji/dino walking.gif", + url: "http://localhost:4001/emoji/dino%20walking.gif", account_ids: [other_user.id, user.id] }, %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs@@ -100,7 +100,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do "name" => "dinosaur", "count" => 1, "me" => true, - "url" => "http://localhost:4001/emoji/dino walking.gif", + "url" => "http://localhost:4001/emoji/dino%20walking.gif", "account_ids" => [other_user.id] } ]