Merge branch 'fork' into multilang

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-05-23 17:37:16 +02:00
commit 1268453a43
23 changed files with 234 additions and 226 deletions

View file

@ -0,0 +1 @@
Fix webfinger spoofing.

View file

@ -0,0 +1 @@
Add "status" notification type

View file

@ -0,0 +1 @@
Fix validate_webfinger when running a different domain for Webfinger

View file

@ -165,10 +165,10 @@ defp cachex_children do
expiration: chat_message_id_idempotency_key_expiration(),
limit: 500_000
),
build_cachex("anti_duplication_mrf", limit: 5_000),
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000),
build_cachex("rel_me", default_ttl: :timer.minutes(30), limit: 2_500),
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("anti_duplication_mrf", limit: 5_000),
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000),
build_cachex("domain", limit: 2500)
]
end

View file

@ -10,7 +10,7 @@ defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode do
def type, do: :string
def cast(language) when is_binary(language) do
if MultiLanguage.is_good_locale_code?(language) do
if MultiLanguage.good_locale_code?(language) do
{:ok, language}
else
{:error, :invalid_language}

View file

@ -9,16 +9,16 @@ defp template(:single), do: Pleroma.Config.get([__MODULE__, :single_line_templat
defp sep(:multi), do: Pleroma.Config.get([__MODULE__, :separator])
defp sep(:single), do: Pleroma.Config.get([__MODULE__, :single_line_separator])
def is_good_locale_code?(code) when is_binary(code), do: code =~ ~r<^[a-zA-Z0-9\-]+$>
def good_locale_code?(code) when is_binary(code), do: code =~ ~r<^[a-zA-Z0-9\-]+$>
def is_good_locale_code?(_code), do: false
def good_locale_code?(_code), do: false
def validate_map(%{} = object) do
{status, data} =
object
|> Enum.reduce({:ok, %{}}, fn
{lang, value}, {status, acc} when is_binary(lang) and is_binary(value) ->
if is_good_locale_code?(lang) do
if good_locale_code?(lang) do
{status, Map.put(acc, lang, value)}
else
{:modified, acc}
@ -60,7 +60,7 @@ defp map_to_str_impl(data, mode) do
def str_to_map(data, opts \\ []) do
with lang when is_binary(lang) <- opts[:lang],
true <- is_good_locale_code?(lang) do
true <- good_locale_code?(lang) do
%{lang => data}
else
_ ->

View file

@ -292,16 +292,22 @@ def set_read_up_to(%{id: user_id} = user, id) do
|> Repo.transaction()
Streamer.stream(["user", "user:notification"], marker)
{:ok, %{marker: marker}}
end
@spec read_one(User.t(), String.t()) ::
{:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
Multi.new()
|> Multi.update(:update, changeset(notification, %{seen: true}))
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
with {:ok, %Notification{} = notification} <- get(user, notification_id),
{:ok, %{marker: marker}} <-
Multi.new()
|> Multi.update(:update, changeset(notification, %{seen: true}))
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction() do
Streamer.stream(["user", "user:notification"], marker)
{:ok, %{marker: marker}}
end
end
@ -607,7 +613,7 @@ def get_notified_participants_from_activity(
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
end
def get_notified_participants_from_activity(_, _), do: {[], []}
def get_notified_participants_from_activity(_, _), do: []
# For some activities, only notify the author of the object
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})

View file

@ -147,7 +147,7 @@ def maybe_add_language(object) do
get_language_from_content_map(object),
get_language_from_content(object)
]
|> Enum.find(&MultiLanguage.is_good_locale_code?(&1))
|> Enum.find(&MultiLanguage.good_locale_code?(&1))
if language do
Map.put(object, "language", language)
@ -186,6 +186,8 @@ defp get_language_from_content(%{"content" => content}) do
defp get_language_from_content(_), do: nil
def maybe_add_content_map(%{"contentMap" => %{}} = object), do: object
def maybe_add_content_map(%{"language" => language, "content" => content} = object)
when not_empty_string(language) do
Map.put(object, "contentMap", Map.put(%{}, language, content))

View file

@ -215,7 +215,7 @@ def fix_context(object) do
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
attachment
|> Enum.filter(fn data -> Map.has_key?(data, "url") end)
|> Enum.filter(fn data -> Map.has_key?(data, "url") or Map.has_key?(data, "href") end)
|> Enum.map(fn data ->
url =
cond do
@ -342,6 +342,7 @@ def fix_tag(%{"tag" => %{} = tag} = object) do
def fix_tag(object), do: object
# prefer content over contentMap
def fix_content_map(%{"content" => content} = object) when not_empty_string(content), do: object
# content map usually only has one language so this will do for now.

View file

@ -151,7 +151,7 @@ defp put_params(draft, params) do
end
defp language(%{params: %{language: language}} = draft) when not is_nil(language) do
if MultiLanguage.is_good_locale_code?(language) do
if MultiLanguage.good_locale_code?(language) do
%__MODULE__{draft | language: language}
else
add_error(
@ -165,7 +165,7 @@ defp language(%{status: status} = draft) when is_binary(status) do
detected_language =
LanguageDetector.detect(draft.status <> " " <> (draft.summary || draft.params[:summary]))
if MultiLanguage.is_good_locale_code?(detected_language) do
if MultiLanguage.good_locale_code?(detected_language) do
%__MODULE__{draft | language: detected_language}
else
draft

View file

@ -108,6 +108,9 @@ def render(
type when type in ["mention", "status", "poll", "pleroma:event_reminder"] ->
put_status(response, activity, reading_user, status_render_opts)
"status" ->
put_status(response, activity, reading_user, status_render_opts)
"favourite" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)

View file

@ -142,7 +142,7 @@ defp maybe_filter_requests(conn) do
end
defp rejected_domains do
Config.get([:instance, :rejected_instances])
Config.get([:instance, :rejected_instances], [])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
end

View file

@ -31,35 +31,44 @@ def get_by_type(type) do
|> Repo.all()
end
def changeset(%__MODULE__{} = webhook, params) do
def changeset(%__MODULE__{} = webhook, params, opts \\ []) do
webhook
|> cast(params, [:url, :events, :enabled])
|> maybe_update_internal(params, opts)
|> validate_required([:url, :events])
|> unique_constraint(:url)
|> strip_events()
|> put_secret()
end
def update_changeset(%__MODULE__{} = webhook, params \\ %{}) do
def update_changeset(%__MODULE__{} = webhook, params \\ %{}, opts \\ []) do
webhook
|> cast(params, [:url, :events, :enabled])
|> maybe_update_internal(params, opts)
|> unique_constraint(:url)
|> strip_events()
end
def create(params) do
defp maybe_update_internal(webhook, params, update_internal: true) do
webhook
|> cast(params, [:internal])
end
defp maybe_update_internal(webhook, _params, _opts), do: webhook
def create(params, opts \\ []) do
{:ok, webhook} =
%__MODULE__{}
|> changeset(params)
|> changeset(params, opts)
|> Repo.insert()
webhook
end
def update(%__MODULE__{} = webhook, params) do
def update(%__MODULE__{} = webhook, params, opts \\ []) do
{:ok, webhook} =
webhook
|> update_changeset(params)
|> update_changeset(params, opts)
|> Repo.update()
webhook

View file

@ -1,7 +1,7 @@
defmodule Pleroma.Mixfile do
use Mix.Project
@build_name "pl"
@build_name "soapbox"
def project do
[

View file

@ -36,7 +36,7 @@ def down do
'reblog',
'favourite',
'pleroma:report',
'poll
'poll',
)
"""
|> execute()

View file

@ -52,7 +52,8 @@ def down do
'favourite',
'pleroma:report',
'poll',
'status'
'status',
'update'
)
"""
|> execute()

View file

@ -0,0 +1,41 @@
{
"subject": "acct:graf@poa.st",
"aliases": [
"https://fba.ryona.agenc/webfingertest"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://fba.ryona.agenc/webfingertest"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://fba.ryona.agenc/webfingertest"
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://fba.ryona.agenc/contact/follow?url={uri}"
},
{
"rel": "http://schemas.google.com/g/2010#updates-from",
"type": "application/atom+xml",
"href": ""
},
{
"rel": "salmon",
"href": "https://fba.ryona.agenc/salmon/friendica"
},
{
"rel": "http://microformats.org/profile/hcard",
"type": "text/html",
"href": "https://fba.ryona.agenc/hcard/friendica"
},
{
"rel": "http://joindiaspora.com/seed_location",
"type": "text/html",
"href": "https://fba.ryona.agenc"
}
]
}

View file

@ -17,6 +17,7 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.Streamer
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
@ -204,6 +205,21 @@ test "doesn't create notification for events without participation approval" do
assert length(user_notifications) == 0
end
test "does not create subscriber notification if mentioned" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{status: "mentioning @#{subscriber.nickname}"})
{:ok, [notification] = notifications} = Notification.create_notifications(status)
assert length(notifications) == 1
assert notification.user_id == subscriber.id
assert notification.type == "mention"
end
test "it sends edited notifications to those who repeated a status" do
user = insert(:user)
repeated_user = insert(:user)
@ -301,93 +317,6 @@ test "create_poll_notifications/1" do
assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
end
describe "CommonApi.post/2 notification-related functionality" do
test_with_mock "creates but does NOT send notification to blocker user",
Push,
[:passthrough],
[] do
user = insert(:user)
blocker = insert(:user)
{:ok, _user_relationship} = User.block(blocker, user)
{:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"})
Pleroma.Tests.ObanHelpers.perform_all()
blocker_id = blocker.id
assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to notification-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
muter = insert(:user)
{:ok, _user_relationships} = User.mute(muter, user)
{:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"})
Pleroma.Tests.ObanHelpers.perform_all()
muter_id = muter.id
assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to thread-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
thread_muter = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"})
Pleroma.Tests.ObanHelpers.perform_all()
[pre_mute_notification] = Repo.all(Notification)
{:ok, _} = CommonAPI.add_mute(thread_muter, activity)
{:ok, _same_context_activity} =
CommonAPI.post(user, %{
status: "hey-hey-hey @#{thread_muter.nickname}!",
in_reply_to_status_id: activity.id
})
Pleroma.Tests.ObanHelpers.perform_all()
[post_mute_notification] =
Repo.all(
from(n in Notification,
where: n.id != ^pre_mute_notification.id and n.user_id == ^thread_muter.id,
order_by: n.id
)
)
pre_mute_notification_id = pre_mute_notification.id
post_mute_notification_id = post_mute_notification.id
assert called(
Push.send(
:meck.is(fn
%Notification{id: ^pre_mute_notification_id} -> true
_ -> false
end)
)
)
refute called(
Push.send(
:meck.is(fn
%Notification{id: ^post_mute_notification_id} -> true
_ -> false
end)
)
)
end
end
describe "create_notification" do
test "it disables notifications from strangers" do
follower = insert(:user)

View file

@ -349,18 +349,6 @@ test "custom emoji urls are URI encoded" do
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
end
test "it adds contentMap if language is specified" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "тест", language: "uk"})
{:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
assert prepared["object"]["contentMap"] == %{
"uk" => "тест"
}
end
test "it prepares a quote post" do
user = insert(:user)

View file

@ -67,7 +67,9 @@ test "edits a webhook", %{conn: conn} do
test "can't edit an internal webhook", %{conn: conn} do
%{id: id} =
Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true})
Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true},
update_internal: true
)
conn
|> put_req_header("content-type", "application/json")

View file

@ -332,4 +332,31 @@ test "muted notification" do
test_notifications_rendering([notification], user, [expected])
end
test "Subscribed status notification" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hi"})
{:ok, [notification]} = Notification.create_notifications(activity)
user = User.get_cached_by_id(user.id)
expected = %{
id: to_string(notification.id),
pleroma: %{is_seen: false, is_muted: false},
type: "status",
account:
AccountView.render("show.json", %{
user: user,
for: subscriber
}),
status: StatusView.render("show.json", %{activity: activity, for: subscriber}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
test_notifications_rendering([notification], subscriber, [expected])
end
end

View file

@ -226,4 +226,18 @@ test "prevents spoofing" do
{:error, _data} = WebFinger.finger("alex@gleasonator.com")
end
end
@tag capture_log: true
test "prevents forgeries" do
Tesla.Mock.mock(fn
%{url: "https://fba.ryona.agency/.well-known/webfinger?resource=acct:graf@fba.ryona.agency"} ->
fake_webfinger =
File.read!("test/fixtures/webfinger/graf-imposter-webfinger.json") |> Jason.decode!()
Tesla.Mock.json(fake_webfinger)
%{url: "https://fba.ryona.agency/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
end)
end
end

View file

@ -1389,27 +1389,6 @@ def get("https://misskey.io/users/83ssedkv53", _, _, _) do
}}
end
def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"),
headers: activitypub_object_headers()
}}
end
def get("https://mitra.social/objects/01830912-1357-d4c5-e4a2-76eab347e749", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/mitra.social_01830912-1357-d4c5-e4a2-76eab347e749.json"
),
headers: activitypub_object_headers()
}}
end
def get("https://gleasonator.com/users/macgirvin", _, _, _) do
{:ok,
%Tesla.Env{
@ -1431,15 +1410,6 @@ def get("https://gleasonator.com/users/macgirvin/collections/featured", _, _, _)
}}
end
def get("https://friends.grishka.me/posts/54642", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
headers: activitypub_object_headers()
}}
end
def get("https://mk.absturztau.be/users/8ozbzjs3o8", _, _, _) do
{:ok,
%Tesla.Env{
@ -1449,15 +1419,6 @@ def get("https://mk.absturztau.be/users/8ozbzjs3o8", _, _, _) do
}}
end
def get("https://friends.grishka.me/users/1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
headers: activitypub_object_headers()
}}
end
def get("https://p.helene.moe/users/helene", _, _, _) do
{:ok,
%Tesla.Env{
@ -1494,31 +1455,69 @@ def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _,
}}
end
def get(
"https://nominatim.openstreetmap.org/search?format=geocodejson&q=Benis&limit=10&accept-language=en&addressdetails=1&namedetails=1",
_,
_,
_
) do
def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/nominatim_search_results.json"),
headers: [{"content-type", "application/json"}]
body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"),
headers: activitypub_object_headers()
}}
end
def get(
"https://nominatim.openstreetmap.org/lookup?format=geocodejson&osm_ids=N3726208425,R3726208425,W3726208425&accept-language=en&addressdetails=1&namedetails=1",
_,
_,
_
) do
def get("https://google.com/", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/google.html")}}
end
def get("https://yahoo.com/", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/yahoo.html")}}
end
def get("https://example.com/error", _, _, _), do: {:error, :overload}
def get("https://example.com/ogp-missing-title", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/nominatim_single_result.json"),
headers: [{"content-type", "application/json"}]
body: File.read!("test/fixtures/rich_media/ogp-missing-title.html")
}}
end
def get("https://example.com/oembed", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")}}
end
def get("https://example.com/oembed.json", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")}}
end
def get("https://example.com/twitter-card", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}}
end
def get("https://example.com/non-ogp", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}}
end
def get("https://example.com/empty", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: "hello"}}
end
def get("https://friends.grishka.me/posts/54642", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
headers: activitypub_object_headers()
}}
end
def get("https://friends.grishka.me/users/1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
headers: activitypub_object_headers()
}}
end
@ -1636,60 +1635,43 @@ def get("https://sub.pleroma.example/users/a", _, _, _) do
}}
end
def get("https://google.com/", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/google.html")}}
end
def get("https://yahoo.com/", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/yahoo.html")}}
end
def get("https://example.com/error", _, _, _), do: {:error, :overload}
def get("https://example.com/ogp-missing-title", _, _, _) do
def get("https://mitra.social/objects/01830912-1357-d4c5-e4a2-76eab347e749", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/rich_media/ogp-missing-title.html")
}}
end
def get("https://example.com/oembed", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")}}
end
def get("https://example.com/oembed.json", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")}}
end
def get("https://example.com/twitter-card", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}}
end
def get("https://example.com/non-ogp", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}}
end
def get("https://example.com/empty", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: "hello"}}
end
def get("https://friends.grishka.me/posts/54642", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
body:
File.read!(
"test/fixtures/tesla_mock/mitra.social_01830912-1357-d4c5-e4a2-76eab347e749.json"
),
headers: activitypub_object_headers()
}}
end
def get("https://friends.grishka.me/users/1", _, _, _) do
def get(
"https://nominatim.openstreetmap.org/search?format=geocodejson&q=Benis&limit=10&accept-language=en&addressdetails=1&namedetails=1",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
headers: activitypub_object_headers()
body: File.read!("test/fixtures/tesla_mock/nominatim_search_results.json"),
headers: [{"content-type", "application/json"}]
}}
end
def get(
"https://nominatim.openstreetmap.org/lookup?format=geocodejson&osm_ids=N3726208425,R3726208425,W3726208425&accept-language=en&addressdetails=1&namedetails=1",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/nominatim_single_result.json"),
headers: [{"content-type", "application/json"}]
}}
end