Fix regex usage in MRF (#254)
fixes #235 fixes #228 Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk> Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/254
This commit is contained in:
parent
5123b3a5dd
commit
31ad09010e
5 changed files with 72 additions and 56 deletions
|
@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## UPGRADE NOTES
|
||||||
|
- Change your instance blocks to remove any `*.` prefixes. `example.com` will block `*.example.com` by default now
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
- Officially supported docker release
|
- Officially supported docker release
|
||||||
- Ability to remove followers unilaterally without a block
|
- Ability to remove followers unilaterally without a block
|
||||||
|
@ -14,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
## Changes
|
## Changes
|
||||||
- Follows no longer override domain blocks, a domain block is final
|
- Follows no longer override domain blocks, a domain block is final
|
||||||
- Deletes are now the lowest priority to publish and will be handled after creates
|
- Deletes are now the lowest priority to publish and will be handled after creates
|
||||||
|
- Domain blocks are now subdomain-matches by default
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
- Registrations via ldap are now compatible with the latest OTP24
|
- Registrations via ldap are now compatible with the latest OTP24
|
||||||
|
|
|
@ -149,9 +149,18 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
||||||
defp get_policies(policies) when is_list(policies), do: policies
|
defp get_policies(policies) when is_list(policies), do: policies
|
||||||
defp get_policies(_), do: []
|
defp get_policies(_), do: []
|
||||||
|
|
||||||
|
# Matches the following:
|
||||||
|
# - https://baddomain.net
|
||||||
|
# - https://extra.baddomain.net/
|
||||||
|
# Does NOT match the following:
|
||||||
|
# - https://maybebaddomain.net/
|
||||||
|
def subdomain_regex(domain) do
|
||||||
|
~r/^(.+\.)?#{Regex.escape(domain)}$/i
|
||||||
|
end
|
||||||
|
|
||||||
@spec subdomains_regex([String.t()]) :: [Regex.t()]
|
@spec subdomains_regex([String.t()]) :: [Regex.t()]
|
||||||
def subdomains_regex(domains) when is_list(domains) do
|
def subdomains_regex(domains) when is_list(domains) do
|
||||||
for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)i
|
Enum.map(domains, &subdomain_regex/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec subdomain_match?([Regex.t()], String.t()) :: boolean()
|
@spec subdomain_match?([Regex.t()], String.t()) :: boolean()
|
||||||
|
|
|
@ -444,17 +444,20 @@ defmodule Pleroma.UserTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
setup do:
|
setup do:
|
||||||
clear_config(:mrf_simple,
|
clear_config(
|
||||||
media_removal: [],
|
[:mrf_simple],
|
||||||
media_nsfw: [],
|
%{
|
||||||
federated_timeline_removal: [],
|
media_removal: [],
|
||||||
report_removal: [],
|
media_nsfw: [],
|
||||||
reject: [],
|
federated_timeline_removal: [],
|
||||||
followers_only: [],
|
report_removal: [],
|
||||||
accept: [],
|
reject: [],
|
||||||
avatar_removal: [],
|
followers_only: [],
|
||||||
banner_removal: [],
|
accept: [],
|
||||||
reject_deletes: []
|
avatar_removal: [],
|
||||||
|
banner_removal: [],
|
||||||
|
reject_deletes: []
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
setup do:
|
setup do:
|
||||||
|
@ -1324,7 +1327,7 @@ defmodule Pleroma.UserTest do
|
||||||
collateral_user =
|
collateral_user =
|
||||||
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
|
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
|
||||||
|
|
||||||
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
|
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
|
||||||
|
|
||||||
refute User.blocks?(user, collateral_user)
|
refute User.blocks?(user, collateral_user)
|
||||||
end
|
end
|
||||||
|
@ -1342,7 +1345,7 @@ defmodule Pleroma.UserTest do
|
||||||
|
|
||||||
user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
|
user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
|
||||||
|
|
||||||
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
|
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
|
||||||
|
|
||||||
assert User.blocks?(user, user_from_subdomain)
|
assert User.blocks?(user, user_from_subdomain)
|
||||||
assert User.blocks?(user, user_with_two_subdomains)
|
assert User.blocks?(user, user_with_two_subdomains)
|
||||||
|
|
|
@ -46,8 +46,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "match with wildcard domain" do
|
test "match with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :media_removal], [{"*.remote.instance", "Whatever reason"}])
|
clear_config([:mrf_simple, :media_removal], [{"remote.instance", "Whatever reason"}])
|
||||||
media_message = build_media_message()
|
media_message = build_media_message("sub.remote.instance")
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
|
|
||||||
assert SimplePolicy.filter(media_message) ==
|
assert SimplePolicy.filter(media_message) ==
|
||||||
|
@ -81,8 +81,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "match with wildcard domain" do
|
test "match with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :media_nsfw], [{"*.remote.instance", "yeah yeah"}])
|
clear_config([:mrf_simple, :media_nsfw], [{"remote.instance", "yeah yeah"}])
|
||||||
media_message = build_media_message()
|
media_message = build_media_message("sub.remote.instance")
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
|
|
||||||
assert SimplePolicy.filter(media_message) ==
|
assert SimplePolicy.filter(media_message) ==
|
||||||
|
@ -92,9 +92,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_media_message do
|
defp build_media_message(domain \\ "remote.instance") do
|
||||||
%{
|
%{
|
||||||
"actor" => "https://remote.instance/users/bob",
|
"actor" => "https://#{domain}/users/bob",
|
||||||
"type" => "Create",
|
"type" => "Create",
|
||||||
"object" => %{
|
"object" => %{
|
||||||
"attachment" => [%{}],
|
"attachment" => [%{}],
|
||||||
|
@ -124,8 +124,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "match with wildcard domain" do
|
test "match with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :report_removal], [{"*.remote.instance", "suya"}])
|
clear_config([:mrf_simple, :report_removal], [{"remote.instance", "suya"}])
|
||||||
report_message = build_report_message()
|
report_message = build_report_message("sub.remote.instance")
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
|
|
||||||
assert {:reject, _} = SimplePolicy.filter(report_message)
|
assert {:reject, _} = SimplePolicy.filter(report_message)
|
||||||
|
@ -133,9 +133,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_report_message do
|
defp build_report_message(domain \\ "remote.instance") do
|
||||||
%{
|
%{
|
||||||
"actor" => "https://remote.instance/users/bob",
|
"actor" => "https://#{domain}/users/bob",
|
||||||
"type" => "Flag"
|
"type" => "Flag"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -143,7 +143,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
describe "when :federated_timeline_removal" do
|
describe "when :federated_timeline_removal" do
|
||||||
test "is empty" do
|
test "is empty" do
|
||||||
clear_config([:mrf_simple, :federated_timeline_removal], [])
|
clear_config([:mrf_simple, :federated_timeline_removal], [])
|
||||||
{_, ftl_message} = build_ftl_actor_and_message()
|
{_, ftl_message} = build_ftl_actor_and_message("https://remote.instance/users/bob")
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
|
|
||||||
assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
|
assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
|
||||||
|
@ -151,7 +151,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "has a matching host" do
|
test "has a matching host" do
|
||||||
{actor, ftl_message} = build_ftl_actor_and_message()
|
{actor, ftl_message} = build_ftl_actor_and_message("https://remote.instance/users/bob")
|
||||||
|
|
||||||
ftl_message_actor_host =
|
ftl_message_actor_host =
|
||||||
ftl_message
|
ftl_message
|
||||||
|
@ -172,7 +172,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "match with wildcard domain" do
|
test "match with wildcard domain" do
|
||||||
{actor, ftl_message} = build_ftl_actor_and_message()
|
{actor, ftl_message} = build_ftl_actor_and_message("https://sub.remote.instance/users/bob")
|
||||||
|
|
||||||
ftl_message_actor_host =
|
ftl_message_actor_host =
|
||||||
ftl_message
|
ftl_message
|
||||||
|
@ -181,7 +181,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
|> Map.fetch!(:host)
|
|> Map.fetch!(:host)
|
||||||
|
|
||||||
clear_config([:mrf_simple, :federated_timeline_removal], [
|
clear_config([:mrf_simple, :federated_timeline_removal], [
|
||||||
{"*." <> ftl_message_actor_host, "owo"}
|
{ftl_message_actor_host, "owo"}
|
||||||
])
|
])
|
||||||
|
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
|
@ -196,7 +196,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "has a matching host but only as:Public in to" do
|
test "has a matching host but only as:Public in to" do
|
||||||
{_actor, ftl_message} = build_ftl_actor_and_message()
|
{_actor, ftl_message} = build_ftl_actor_and_message("https://remote.instance/users/bob")
|
||||||
|
|
||||||
ftl_message_actor_host =
|
ftl_message_actor_host =
|
||||||
ftl_message
|
ftl_message
|
||||||
|
@ -253,8 +253,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_ftl_actor_and_message do
|
defp build_ftl_actor_and_message(ap_id) do
|
||||||
actor = insert(:user)
|
actor = insert(:user, ap_id: ap_id)
|
||||||
|
|
||||||
{actor,
|
{actor,
|
||||||
%{
|
%{
|
||||||
|
@ -282,9 +282,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "activity matches with wildcard domain" do
|
test "activity matches with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :reject], [{"*.remote.instance", ""}])
|
clear_config([:mrf_simple, :reject], [{"remote.instance", ""}])
|
||||||
|
|
||||||
remote_message = build_remote_message()
|
remote_message = build_remote_message("sub.remote.instance")
|
||||||
|
|
||||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||||
end
|
end
|
||||||
|
@ -325,7 +325,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
describe "when :followers_only" do
|
describe "when :followers_only" do
|
||||||
test "is empty" do
|
test "is empty" do
|
||||||
clear_config([:mrf_simple, :followers_only], [])
|
clear_config([:mrf_simple, :followers_only], [])
|
||||||
{_, ftl_message} = build_ftl_actor_and_message()
|
{_, ftl_message} = build_ftl_actor_and_message("https://remote.instance/users/alice")
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
|
|
||||||
assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
|
assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
|
||||||
|
@ -412,10 +412,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "activity matches with wildcard domain" do
|
test "activity matches with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :accept], [{"*.remote.instance", ""}])
|
clear_config([:mrf_simple, :accept], [{"remote.instance", ""}])
|
||||||
|
|
||||||
local_message = build_local_message()
|
local_message = build_local_message()
|
||||||
remote_message = build_remote_message()
|
remote_message = build_remote_message("sub.remote.instance")
|
||||||
|
|
||||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||||
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||||
|
@ -457,9 +457,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "match with wildcard domain" do
|
test "match with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :avatar_removal], [{"*.remote.instance", ""}])
|
clear_config([:mrf_simple, :avatar_removal], [{"remote.instance", ""}])
|
||||||
|
|
||||||
remote_user = build_remote_user()
|
remote_user = build_remote_user("sub.remote.instance")
|
||||||
{:ok, filtered} = SimplePolicy.filter(remote_user)
|
{:ok, filtered} = SimplePolicy.filter(remote_user)
|
||||||
|
|
||||||
refute filtered["icon"]
|
refute filtered["icon"]
|
||||||
|
@ -493,9 +493,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "match with wildcard domain" do
|
test "match with wildcard domain" do
|
||||||
clear_config([:mrf_simple, :banner_removal], [{"*.remote.instance", ""}])
|
clear_config([:mrf_simple, :banner_removal], [{"remote.instance", ""}])
|
||||||
|
|
||||||
remote_user = build_remote_user()
|
remote_user = build_remote_user("sub.remote.instance")
|
||||||
{:ok, filtered} = SimplePolicy.filter(remote_user)
|
{:ok, filtered} = SimplePolicy.filter(remote_user)
|
||||||
|
|
||||||
refute filtered["image"]
|
refute filtered["image"]
|
||||||
|
@ -553,10 +553,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when :reject_deletes match with wildcard domain" do
|
describe "when :reject_deletes match with wildcard domain" do
|
||||||
setup do: clear_config([:mrf_simple, :reject_deletes], [{"*.remote.instance", ""}])
|
setup do: clear_config([:mrf_simple, :reject_deletes], [{"remote.instance", ""}])
|
||||||
|
|
||||||
test "it rejects the deletion" do
|
test "it rejects the deletion" do
|
||||||
deletion_message = build_remote_deletion_message()
|
deletion_message = build_remote_deletion_message("sub.remote.instance")
|
||||||
|
|
||||||
assert {:reject, _} = SimplePolicy.filter(deletion_message)
|
assert {:reject, _} = SimplePolicy.filter(deletion_message)
|
||||||
end
|
end
|
||||||
|
@ -570,13 +570,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_remote_message do
|
defp build_remote_message(domain \\ "remote.instance") do
|
||||||
%{"actor" => "https://remote.instance/users/bob"}
|
%{"actor" => "https://#{domain}/users/bob"}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_remote_user do
|
defp build_remote_user(domain \\ "remote.instance") do
|
||||||
%{
|
%{
|
||||||
"id" => "https://remote.instance/users/bob",
|
"id" => "https://#{domain}/users/bob",
|
||||||
"icon" => %{
|
"icon" => %{
|
||||||
"url" => "http://example.com/image.jpg",
|
"url" => "http://example.com/image.jpg",
|
||||||
"type" => "Image"
|
"type" => "Image"
|
||||||
|
@ -589,10 +589,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_remote_deletion_message do
|
defp build_remote_deletion_message(domain \\ "remote.instance") do
|
||||||
%{
|
%{
|
||||||
"type" => "Delete",
|
"type" => "Delete",
|
||||||
"actor" => "https://remote.instance/users/bob"
|
"actor" => "https://#{domain}/users/bob"
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
||||||
|
|
||||||
test "subdomains_regex/1" do
|
test "subdomains_regex/1" do
|
||||||
assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [
|
assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [
|
||||||
~r/^unsafe.tld$/i,
|
~r/^(.+\.)?unsafe\.tld$/i,
|
||||||
~r/^(.*\.)*unsafe.tld$/i
|
~r/^(.+\.)?\*\.unsafe\.tld$/i
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
||||||
test "common domains" do
|
test "common domains" do
|
||||||
regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"])
|
regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"])
|
||||||
|
|
||||||
assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i]
|
assert regexes == [~r/^(.+\.)?unsafe\.tld$/i, ~r/^(.+\.)?unsafe2\.tld$/i]
|
||||||
|
|
||||||
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||||
assert MRF.subdomain_match?(regexes, "unsafe2.tld")
|
assert MRF.subdomain_match?(regexes, "unsafe2.tld")
|
||||||
|
@ -27,9 +27,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "wildcard domains with one subdomain" do
|
test "wildcard domains with one subdomain" do
|
||||||
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
|
regexes = MRF.subdomains_regex(["unsafe.tld"])
|
||||||
|
|
||||||
assert regexes == [~r/^(.*\.)*unsafe.tld$/i]
|
assert regexes == [~r/^(.+\.)?unsafe\.tld$/i]
|
||||||
|
|
||||||
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||||
assert MRF.subdomain_match?(regexes, "sub.unsafe.tld")
|
assert MRF.subdomain_match?(regexes, "sub.unsafe.tld")
|
||||||
|
@ -38,9 +38,9 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "wildcard domains with two subdomains" do
|
test "wildcard domains with two subdomains" do
|
||||||
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
|
regexes = MRF.subdomains_regex(["unsafe.tld"])
|
||||||
|
|
||||||
assert regexes == [~r/^(.*\.)*unsafe.tld$/i]
|
assert regexes == [~r/^(.+\.)?unsafe\.tld$/i]
|
||||||
|
|
||||||
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||||
assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld")
|
assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld")
|
||||||
|
@ -51,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
||||||
test "matches are case-insensitive" do
|
test "matches are case-insensitive" do
|
||||||
regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"])
|
regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"])
|
||||||
|
|
||||||
assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i]
|
assert regexes == [~r/^(.+\.)?UnSafe\.TLD$/i, ~r/^(.+\.)?UnSAFE2\.Tld$/i]
|
||||||
|
|
||||||
assert MRF.subdomain_match?(regexes, "UNSAFE.TLD")
|
assert MRF.subdomain_match?(regexes, "UNSAFE.TLD")
|
||||||
assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD")
|
assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD")
|
||||||
|
|
Loading…
Reference in a new issue