Add link verification in profile fields (#405)
authorfloatingghost <hannah@coffee-and-dreams.uk>
Thu, 29 Dec 2022 20:56:06 +0000 (20:56 +0000)
committerfloatingghost <hannah@coffee-and-dreams.uk>
Thu, 29 Dec 2022 20:56:06 +0000 (20:56 +0000)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/405

CHANGELOG.md
lib/pleroma/application.ex
lib/pleroma/user.ex
lib/pleroma/web/rel_me.ex
test/pleroma/web/mastodon_api/update_credentials_test.exs

index d556b39c38bf467e92f2d542a0550d2994b065df..ea6a25e4bd326950c6c45e6cf7b538d9796b0ddd 100644 (file)
@@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Prometheus metrics exporting from `/api/v1/akkoma/metrics`
 - Ability to alter http pool size
 - Translation of statuses via ArgosTranslate
+- Ability to "verify" links in profile fields via rel=me
+- Mix tasks to dump/load config to/from json for bulk editing
 
 ### Removed
 - Non-finch HTTP adapters
index 26b500dc832eccd63241fcf5981b2aa84915f869..0273972bed96c425c75a15dd1d461a52cb285057 100644 (file)
@@ -159,7 +159,8 @@ defmodule Pleroma.Application do
       build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
       build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
       build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
-      build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000)
+      build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
+      build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300)
     ]
   end
 
index d7c1511ce0acb98ea2d6610ca629785915d2185d..2d4bd097db5f64b718ad8dd43cfd6de85415b900 100644 (file)
@@ -479,7 +479,7 @@ defmodule Pleroma.User do
     |> validate_format(:nickname, @email_regex)
     |> validate_length(:bio, max: bio_limit)
     |> validate_length(:name, max: name_limit)
-    |> validate_fields(true)
+    |> validate_fields(true, struct)
     |> validate_non_local()
   end
 
@@ -549,7 +549,7 @@ defmodule Pleroma.User do
       :pleroma_settings_store,
       &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
     )
-    |> validate_fields(false)
+    |> validate_fields(false, struct)
   end
 
   defp put_fields(changeset) do
@@ -2359,7 +2359,8 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
-  def validate_fields(changeset, remote? \\ false) do
+  @spec validate_fields(Ecto.Changeset.t(), Boolean.t(), User.t()) :: Ecto.Changeset.t()
+  def validate_fields(changeset, remote? \\ false, struct) do
     limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
     limit = Config.get([:instance, limit_name], 0)
 
@@ -2372,6 +2373,7 @@ defmodule Pleroma.User do
         [fields: "invalid"]
       end
     end)
+    |> maybe_validate_rel_me_field(struct)
   end
 
   defp valid_field?(%{"name" => name, "value" => value}) do
@@ -2384,6 +2386,75 @@ defmodule Pleroma.User do
 
   defp valid_field?(_), do: false
 
+  defp is_url(nil), do: nil
+
+  defp is_url(uri) do
+    case URI.parse(uri) do
+      %URI{host: nil} -> false
+      %URI{scheme: nil} -> false
+      _ -> true
+    end
+  end
+
+  @spec maybe_validate_rel_me_field(Changeset.t(), User.t()) :: Changeset.t()
+  defp maybe_validate_rel_me_field(changeset, %User{ap_id: _ap_id} = struct) do
+    fields = get_change(changeset, :fields)
+    raw_fields = get_change(changeset, :raw_fields)
+
+    if is_nil(fields) do
+      changeset
+    else
+      validate_rel_me_field(changeset, fields, raw_fields, struct)
+    end
+  end
+
+  defp maybe_validate_rel_me_field(changeset, _), do: changeset
+
+  @spec validate_rel_me_field(Changeset.t(), [Map.t()], [Map.t()], User.t()) :: Changeset.t()
+  defp validate_rel_me_field(changeset, fields, raw_fields, %User{
+         nickname: nickname,
+         ap_id: ap_id
+       }) do
+    fields =
+      fields
+      |> Enum.with_index()
+      |> Enum.map(fn {%{"name" => name, "value" => value}, index} ->
+        raw_value =
+          if is_nil(raw_fields) do
+            nil
+          else
+            Enum.at(raw_fields, index)["value"]
+          end
+
+        if is_url(raw_value) do
+          frontend_url =
+            Pleroma.Web.Router.Helpers.redirect_url(
+              Pleroma.Web.Endpoint,
+              :redirector_with_meta,
+              nickname
+            )
+
+          possible_urls = [ap_id, frontend_url]
+
+          with "me" <- RelMe.maybe_put_rel_me(raw_value, possible_urls) do
+            %{
+              "name" => name,
+              "value" => value,
+              "verified_at" => DateTime.to_iso8601(DateTime.utc_now())
+            }
+          else
+            e ->
+              Logger.error("Could not check for rel=me, #{inspect(e)}")
+              %{"name" => name, "value" => value}
+          end
+        else
+          %{"name" => name, "value" => value}
+        end
+      end)
+
+    put_change(changeset, :fields, fields)
+  end
+
   defp truncate_field(%{"name" => name, "value" => value}) do
     {name, _chopped} =
       String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
@@ -2551,11 +2622,8 @@ defmodule Pleroma.User do
   # - display name
   def sanitize_html(%User{} = user, filter) do
     fields =
-      Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
-        %{
-          "name" => name,
-          "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
-        }
+      Enum.map(user.fields, fn %{"value" => value} = field ->
+        Map.put(field, "value", HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly))
       end)
 
     user
index 1826031dd4296676fa3fb9aff9900a7eee9c2fc3..3a1812f7a0e9451b441427420d12a63309c331e3 100644 (file)
@@ -38,12 +38,11 @@ defmodule Pleroma.Web.RelMe do
 
   def maybe_put_rel_me("http" <> _ = target_page, profile_urls) when is_list(profile_urls) do
     {:ok, rel_me_hrefs} = parse(target_page)
-
     true = Enum.any?(rel_me_hrefs, fn x -> x in profile_urls end)
 
     "me"
   rescue
-    _ -> nil
+    e -> nil
   end
 
   def maybe_put_rel_me(_, _) do
index 2ba909dadca93e2192a39ffc46ac97616eb0202c..e9b8825bfb7801f7c5507175ecb2fb9b82dbbf31 100644 (file)
@@ -465,6 +465,69 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
              ]
     end
 
+    test "update fields with a link to content with rel=me, with ap id", %{user: user, conn: conn} do
+      Tesla.Mock.mock(fn
+        %{url: "http://example.com/rel_me/ap_id"} ->
+          %Tesla.Env{
+            status: 200,
+            body: ~s[<html><head><link rel="me" href="#{user.ap_id}"></head></html>]
+          }
+      end)
+
+      field = %{name: "Website", value: "http://example.com/rel_me/ap_id"}
+
+      account_data =
+        conn
+        |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: [field]})
+        |> json_response_and_validate_schema(200)
+
+      assert [
+               %{
+                 "name" => "Website",
+                 "value" =>
+                   ~s[<a href="http://example.com/rel_me/ap_id" rel="ugc">http://example.com/rel_me/ap_id</a>],
+                 "verified_at" => verified_at
+               }
+             ] = account_data["fields"]
+
+      {:ok, verified_at, _} = DateTime.from_iso8601(verified_at)
+      assert DateTime.diff(DateTime.utc_now(), verified_at) < 10
+    end
+
+    test "update fields with a link to content with rel=me, with frontend path", %{
+      user: user,
+      conn: conn
+    } do
+      fe_url = "#{Pleroma.Web.Endpoint.url()}/#{user.nickname}"
+
+      Tesla.Mock.mock(fn
+        %{url: "http://example.com/rel_me/fe_path"} ->
+          %Tesla.Env{
+            status: 200,
+            body: ~s[<html><head><link rel="me" href="#{fe_url}"></head></html>]
+          }
+      end)
+
+      field = %{name: "Website", value: "http://example.com/rel_me/fe_path"}
+
+      account_data =
+        conn
+        |> patch("/api/v1/accounts/update_credentials", %{fields_attributes: [field]})
+        |> json_response_and_validate_schema(200)
+
+      assert [
+               %{
+                 "name" => "Website",
+                 "value" =>
+                   ~s[<a href="http://example.com/rel_me/fe_path" rel="ugc">http://example.com/rel_me/fe_path</a>],
+                 "verified_at" => verified_at
+               }
+             ] = account_data["fields"]
+
+      {:ok, verified_at, _} = DateTime.from_iso8601(verified_at)
+      assert DateTime.diff(DateTime.utc_now(), verified_at) < 10
+    end
+
     test "emojis in fields labels", %{conn: conn} do
       fields = [
         %{name: ":firefox:", value: "is best 2hu"},