diff --git a/config/config.exs b/config/config.exs
index 758661120..109cf6516 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -255,6 +255,7 @@ config :pleroma, :instance,
dynamic_configuration: false,
user_bio_length: 5000,
user_name_length: 100,
+ max_account_fields: 4,
external_user_synchronization: true
config :pleroma, :markup,
diff --git a/docs/config.md b/docs/config.md
index 20311db54..ca5da7db1 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -132,6 +132,7 @@ config :pleroma, Pleroma.Emails.Mailer,
* `skip_thread_containment`: Skip filter out broken threads. The default is `false`.
* `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`.
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
+* `max_account_fields`: The maximum number of custom fields in the user profile (default: `4`)
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 22eb9a182..fa57052fb 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -49,6 +49,7 @@ defmodule Pleroma.User.Info do
field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
field(:pleroma_settings_store, :map, default: %{})
+ field(:fields, {:array, :map}, default: [])
field(:notification_settings, :map,
default: %{
@@ -286,10 +287,32 @@ defmodule Pleroma.User.Info do
:background,
:show_role,
:skip_thread_containment,
+ :fields,
:pleroma_settings_store
])
+ |> validate_fields()
end
+ def validate_fields(changeset) do
+ limit = Pleroma.Config.get([:instance, :max_account_fields], 0)
+
+ changeset
+ |> validate_length(:fields, max: limit)
+ |> validate_change(:fields, fn :fields, fields ->
+ if Enum.all?(fields, &valid_field?/1) do
+ []
+ else
+ [fields: "invalid"]
+ end
+ end)
+ end
+
+ defp valid_field?(%{"name" => name, "value" => value}) do
+ is_binary(name) && is_binary(value)
+ end
+
+ defp valid_field?(_), do: false
+
@spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
def confirmation_changeset(info, opts) do
need_confirmation? = Keyword.get(opts, :need_confirmation)
@@ -384,6 +407,14 @@ defmodule Pleroma.User.Info do
cast(info, params, [:muted_reblogs])
end
+ def fields(%{source_data: %{"attachment" => attachment}}) do
+ attachment
+ |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
+ |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+ end
+
+ def fields(%{fields: fields}), do: fields
+
def follow_information_update(info, params) do
info
|> cast(params, [
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 7ce2b5b06..e79a02caa 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -156,6 +156,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
+ |> add_if_present(params, "fields", :fields, fn fields ->
+ fields =
+ Enum.map(fields, fn field ->
+ %{
+ "name" => Formatter.html_escape(field["name"], "text/plain"),
+ "value" => Formatter.html_escape(field["value"], "text/plain")
+ }
+ end)
+
+ {:ok, fields}
+ end)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
end)
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 72c092f25..d2f3986ff 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -93,10 +93,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
}
end)
- fields =
- (user.info.source_data["attachment"] || [])
- |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
- |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+ fields = User.Info.fields(user.info)
+ fields_html = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
@@ -119,11 +117,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
header: header,
header_static: header,
emojis: emojis,
- fields: fields,
+ fields: fields_html,
bot: bot,
source: %{
note: HTML.strip_tags((user.bio || "") |> String.replace("
", "\n")),
sensitive: false,
+ fields: fields,
pleroma: %{}
},
diff --git a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs
index 71d0c8af8..a3eadde16 100644
--- a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs
@@ -300,5 +300,44 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert user["display_name"] == name
assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"]
end
+
+ test "update fields", %{conn: conn} do
+ user = insert(:user)
+
+ fields = [
+ %{"name" => "foo", "value" => "bar"},
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ account =
+ conn
+ |> assign(:user, user)
+ |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields})
+ |> json_response(200)
+
+ assert account["fields"] == [
+ %{"name" => "<b>foo<b>", "value" => "<i>bar</i>"},
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ assert account["source"]["fields"] == [
+ %{"name" => "<b>foo<b>", "value" => "<i>bar</i>"},
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ Pleroma.Config.put([:instance, :max_account_fields], 1)
+
+ fields = [
+ %{"name" => "foo", "value" => "bar"},
+ %{"name" => "link", "value" => "cofe.io"}
+ ]
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields})
+
+ assert %{"error" => "Invalid request"} == json_response(conn, 403)
+ end
end
end