aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/config_db.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/config_db.ex')
-rw-r--r--lib/pleroma/config_db.ex434
1 files changed, 181 insertions, 253 deletions
diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex
index cb57673e3..7a29096c5 100644
--- a/lib/pleroma/config_db.ex
+++ b/lib/pleroma/config_db.ex
@@ -6,8 +6,7 @@ defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
- import Ecto.Query, only: [select: 3, from: 2]
- import Pleroma.Web.Gettext
+ import Ecto.Query, only: [from: 2]
alias __MODULE__
alias Pleroma.Repo
@@ -22,6 +21,10 @@ defmodule Pleroma.ConfigDB do
{:pleroma, :mrf_keyword, :replace}
]
+ @groups_without_keys [:quack, :mime, :cors_plug, :esshd, :ex_aws, :joken, :logger, :swoosh]
+
+ @pleroma_not_keyword_values [Pleroma.Web.Auth.Authenticator, :admin_token]
+
schema "config" do
field(:key, Pleroma.EctoType.Config.Atom)
field(:group, Pleroma.EctoType.Config.Atom)
@@ -31,13 +34,35 @@ defmodule Pleroma.ConfigDB do
timestamps()
end
- @spec get_all_as_keyword() :: keyword()
- def get_all_as_keyword do
- ConfigDB
- |> select([c], {c.group, c.key, c.value})
- |> Repo.all()
- |> Enum.reduce([], fn {group, key, value}, acc ->
- Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
+ @spec all() :: [t()]
+ def all, do: Repo.all(ConfigDB)
+
+ @spec all_with_db() :: [t()]
+ def all_with_db do
+ all()
+ |> Enum.map(fn
+ %{group: :pleroma, key: key} = change when key in @pleroma_not_keyword_values ->
+ %{change | db: [change.key]}
+
+ %{value: value} = change ->
+ %{change | db: Keyword.keys(value)}
+ end)
+ end
+
+ @spec all_as_keyword() :: keyword()
+ def all_as_keyword do
+ all()
+ |> as_keyword()
+ end
+
+ @spec as_keyword([t()]) :: keyword()
+ def as_keyword(changes) do
+ Enum.reduce(changes, [], fn
+ %{group: group, key: nil, value: value}, acc ->
+ Keyword.update(acc, group, value, &Keyword.merge(&1, value))
+
+ %{group: group, key: key, value: value}, acc ->
+ Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
end)
end
@@ -52,14 +77,22 @@ defmodule Pleroma.ConfigDB do
end
@spec get_by_params(map()) :: ConfigDB.t() | nil
- def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
+ def get_by_params(%{group: group, key: key} = params)
+ when not is_nil(key) and not is_nil(group) do
+ Repo.get_by(ConfigDB, params)
+ end
+
+ def get_by_params(%{group: group}) do
+ from(c in ConfigDB, where: c.group == ^group and is_nil(c.key)) |> Repo.one()
+ end
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
- |> validate_required([:key, :group, :value])
+ |> validate_required([:group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
+ |> unique_constraint(:key, name: :config_group__key_is_null_index)
end
defp create(params) do
@@ -74,319 +107,214 @@ defmodule Pleroma.ConfigDB do
|> Repo.update()
end
- @spec get_db_keys(keyword(), any()) :: [String.t()]
- def get_db_keys(value, key) do
- keys =
- if Keyword.keyword?(value) do
- Keyword.keys(value)
- else
- [key]
- end
-
- Enum.map(keys, &to_json_types(&1))
- end
-
- @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
- def merge_group(group, key, old_value, new_value) do
- new_keys = to_mapset(new_value)
-
- intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
-
- merged_value = ConfigDB.merge(old_value, new_value)
-
- @full_subkey_update
- |> Enum.map(fn
- {g, k, subkey} when g == group and k == key ->
- if subkey in intersect_keys, do: subkey, else: []
-
- _ ->
- []
- end)
- |> List.flatten()
- |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
- end
-
- defp to_mapset(keyword) do
- keyword
- |> Keyword.keys()
- |> MapSet.new()
- end
-
- @spec sub_key_full_update?(atom(), atom(), [Keyword.key()]) :: boolean()
- def sub_key_full_update?(group, key, subkeys) do
- Enum.any?(@full_subkey_update, fn {g, k, subkey} ->
- g == group and k == key and subkey in subkeys
- end)
- end
-
- @spec merge(keyword(), keyword()) :: keyword()
- def merge(config1, config2) when is_list(config1) and is_list(config2) do
- Keyword.merge(config1, config2, fn _, app1, app2 ->
- if Keyword.keyword?(app1) and Keyword.keyword?(app2) do
- Keyword.merge(app1, app2, &deep_merge/3)
- else
- app2
- end
- end)
- end
-
- defp deep_merge(_key, value1, value2) do
- if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
- Keyword.merge(value1, value2, &deep_merge/3)
- else
- value2
- end
- end
-
+ @doc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update_or_create(params) do
- params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
- with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
- {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config},
- {_, true, config} <-
- {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do
+ with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts) do
new_value = merge_group(config.group, config.key, config.value, params[:value])
+
update(config, %{value: new_value})
else
- {reason, false, config} when reason in [:partial_update, :can_be_merged] ->
- update(config, params)
-
nil ->
create(params)
end
end
- defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
-
- defp only_full_update?(%ConfigDB{group: group, key: key}) do
- full_key_update = [
- {:pleroma, :ecto_repos},
- {:quack, :meta},
- {:mime, :types},
- {:cors_plug, [:max_age, :methods, :expose, :headers]},
- {:swarm, :node_blacklist},
- {:logger, :backends}
- ]
-
- Enum.any?(full_key_update, fn
- {s_group, s_key} ->
- group == s_group and ((is_list(s_key) and key in s_key) or key == s_key)
- end)
- end
-
+ @doc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
- def delete(params) do
- search_opts = Map.delete(params, :subkeys)
+ @doc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
+ @spec delete_or_update(map()) :: {:ok, t()} | {:ok, nil} | {:error, Changeset.t()}
+ def delete_or_update(%{group: _, key: key} = params) when not is_nil(key) do
+ search_opts = Map.take(params, [:group, :key])
- with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
- {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
- keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)),
- {_, config, new_value} when new_value != [] <-
- {:partial_remove, config, Keyword.drop(config.value, keys)} do
- update(config, %{value: new_value})
+ with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts) do
+ do_delete_or_update(config, params[:subkeys])
else
- {:partial_remove, config, []} ->
- Repo.delete(config)
-
- {config, nil} ->
- Repo.delete(config)
+ _ -> {:ok, nil}
+ end
+ end
- nil ->
- err =
- dgettext("errors", "Config with params %{params} not found", params: inspect(params))
+ def delete_or_update(%{group: group}) do
+ query = from(c in ConfigDB, where: c.group == ^group)
- {:error, err}
+ with {num, _} <- Repo.delete_all(query) do
+ {:ok, num}
end
end
- @spec to_json_types(term()) :: map() | list() | boolean() | String.t()
- def to_json_types(entity) when is_list(entity) do
- Enum.map(entity, &to_json_types/1)
+ defp do_delete_or_update(%ConfigDB{} = config, subkeys)
+ when is_list(subkeys) and subkeys != [] do
+ new_value = Keyword.drop(config.value, subkeys)
+
+ if new_value == [] do
+ delete(config)
+ else
+ update(config, %{value: new_value})
+ end
end
- def to_json_types(%Regex{} = entity), do: inspect(entity)
+ defp do_delete_or_update(%ConfigDB{} = config, _), do: delete(config)
- def to_json_types(entity) when is_map(entity) do
- Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
- end
+ defp merge_group(group, key, old_value, new_value)
+ when is_list(old_value) and is_list(new_value) do
+ new_keys = to_mapset(new_value)
- def to_json_types({:args, args}) when is_list(args) do
- arguments =
- Enum.map(args, fn
- arg when is_tuple(arg) -> inspect(arg)
- arg -> to_json_types(arg)
- end)
+ intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
- %{"tuple" => [":args", arguments]}
- end
+ merged_value = deep_merge(old_value, new_value)
- def to_json_types({:proxy_url, {type, :localhost, port}}) do
- %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
+ @full_subkey_update
+ |> Enum.reduce([], fn
+ {g, k, subkey}, acc when g == group and k == key ->
+ if subkey in intersect_keys do
+ [subkey | acc]
+ else
+ acc
+ end
+
+ _, acc ->
+ acc
+ end)
+ |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
end
- def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
- ip =
- host
- |> :inet_parse.ntoa()
- |> to_string()
+ defp merge_group(_group, _key, _old_value, new_value) when is_list(new_value), do: new_value
- %{
- "tuple" => [
- ":proxy_url",
- %{"tuple" => [to_json_types(type), ip, port]}
- ]
- }
+ defp merge_group(:pleroma, key, _old_value, new_value)
+ when key in @pleroma_not_keyword_values do
+ new_value
end
- def to_json_types({:proxy_url, {type, host, port}}) do
- %{
- "tuple" => [
- ":proxy_url",
- %{"tuple" => [to_json_types(type), to_string(host), port]}
- ]
- }
+ defp to_mapset(keyword) when is_list(keyword) do
+ keyword
+ |> Keyword.keys()
+ |> MapSet.new()
end
- def to_json_types({:partial_chain, entity}),
- do: %{"tuple" => [":partial_chain", inspect(entity)]}
-
- def to_json_types(entity) when is_tuple(entity) do
- value =
- entity
- |> Tuple.to_list()
- |> to_json_types()
-
- %{"tuple" => value}
+ defp deep_merge(config1, config2) when is_list(config1) and is_list(config2) do
+ Keyword.merge(config1, config2, fn _, app1, app2 ->
+ if Keyword.keyword?(app1) and Keyword.keyword?(app2) do
+ Keyword.merge(app1, app2, &deep_merge/3)
+ else
+ app2
+ end
+ end)
end
- def to_json_types(entity) when is_binary(entity), do: entity
+ defp deep_merge(_key, value1, value2) do
+ if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
+ Keyword.merge(value1, value2, &deep_merge/3)
+ else
+ value2
+ end
+ end
- def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
- entity
+ @spec reduce_defaults_and_merge_with_changes([t()], keyword()) :: {[t()], keyword()}
+ def reduce_defaults_and_merge_with_changes(changes, defaults) do
+ Enum.reduce(changes, {[], defaults}, &reduce_default_and_merge_with_change/2)
end
- def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
- ":#{entity}"
+ defp reduce_default_and_merge_with_change(%{group: group} = change, {acc, defaults})
+ when group in @groups_without_keys do
+ {default, remaining_defaults} = Keyword.pop(defaults, group)
+
+ change = merge_change_with_default(change, default)
+ {[change | acc], remaining_defaults}
end
- def to_json_types(entity) when is_atom(entity), do: inspect(entity)
+ defp reduce_default_and_merge_with_change(%{group: group, key: key} = change, {acc, defaults}) do
+ if defaults[group] do
+ {default, remaining_group_defaults} = Keyword.pop(defaults[group], key)
- @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
- def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
- arguments =
- Enum.map(args, fn arg ->
- if String.contains?(arg, ["{", "}"]) do
- {elem, []} = Code.eval_string(arg)
- elem
+ remaining_defaults =
+ if remaining_group_defaults == [] do
+ Keyword.delete(defaults, group)
else
- to_elixir_types(arg)
+ Keyword.put(defaults, group, remaining_group_defaults)
end
- end)
-
- {:args, arguments}
- end
- def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
- {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}}
- end
-
- def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
- {partial_chain, []} =
- entity
- |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
- |> Code.eval_string()
+ change = merge_change_with_default(change, default)
- {:partial_chain, partial_chain}
+ {[change | acc], remaining_defaults}
+ else
+ {[change | acc], defaults}
+ end
end
- def to_elixir_types(%{"tuple" => entity}) do
- Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
+ @spec from_keyword_to_structs(keyword(), [] | [t()]) :: [t()]
+ def from_keyword_to_structs(keyword, initial_acc \\ []) do
+ Enum.reduce(keyword, initial_acc, &reduce_to_structs/2)
end
- def to_elixir_types(entity) when is_map(entity) do
- Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
+ defp reduce_to_structs({group, config}, group_acc) when group in @groups_without_keys do
+ [struct(%ConfigDB{}, to_map(group, config)) | group_acc]
end
- def to_elixir_types(entity) when is_list(entity) do
- Enum.map(entity, &to_elixir_types/1)
+ defp reduce_to_structs({group, config}, group_acc) do
+ Enum.reduce(config, group_acc, fn {key, value}, acc ->
+ [struct(%ConfigDB{}, to_map(group, key, value)) | acc]
+ end)
end
- def to_elixir_types(entity) when is_binary(entity) do
- entity
- |> String.trim()
- |> string_to_elixir_types()
+ @spec from_keyword_to_maps(keyword(), [] | [map()]) :: [map()]
+ def from_keyword_to_maps(keyword, initial_acc \\ []) do
+ Enum.reduce(keyword, initial_acc, &reduce_to_maps/2)
end
- def to_elixir_types(entity), do: entity
-
- @spec string_to_elixir_types(String.t()) ::
- atom() | Regex.t() | module() | String.t() | no_return()
- def string_to_elixir_types("~r" <> _pattern = regex) do
- pattern =
- ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
-
- delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
-
- with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
- Regex.named_captures(pattern, regex),
- {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
- {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
- result
- end
+ defp reduce_to_maps({group, config}, group_acc) when group in @groups_without_keys do
+ [to_map(group, config) | group_acc]
end
- def string_to_elixir_types(":" <> atom), do: String.to_atom(atom)
-
- def string_to_elixir_types(value) do
- if module_name?(value) do
- String.to_existing_atom("Elixir." <> value)
- else
- value
- end
+ defp reduce_to_maps({group, config}, group_acc) do
+ Enum.reduce(config, group_acc, fn {key, value}, acc ->
+ [to_map(group, key, value) | acc]
+ end)
end
- defp parse_host("localhost"), do: :localhost
+ defp to_map(group, config), do: %{group: group, value: config}
- defp parse_host(host) do
- charlist = to_charlist(host)
+ defp to_map(group, key, value), do: %{group: group, key: key, value: value}
- case :inet.parse_address(charlist) do
- {:error, :einval} ->
- charlist
+ @spec merge_changes_with_defaults([t()], keyword()) :: [t()]
+ def merge_changes_with_defaults(changes, defaults) when is_list(changes) do
+ Enum.map(changes, fn
+ %{group: group} = change when group in @groups_without_keys ->
+ merge_change_with_default(change, defaults[group])
- {:ok, ip} ->
- ip
- end
+ %{group: group, key: key} = change ->
+ merge_change_with_default(change, defaults[group][key])
+ end)
end
- defp find_valid_delimiter([], _string, _) do
- raise(ArgumentError, message: "valid delimiter for Regex expression not found")
+ defp merge_change_with_default(change, default) do
+ %{change | value: merge_change_value_with_default(change, default)}
end
- defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter)
- when is_tuple(delimiter) do
- if String.contains?(pattern, closing) do
- find_valid_delimiter(others, pattern, regex_delimiter)
+ @spec merge_change_value_with_default(t(), keyword()) :: keyword()
+ def merge_change_value_with_default(change, default) do
+ if Ecto.get_meta(change, :state) == :deleted do
+ default
else
- {:ok, {leading, closing}}
+ merge_group(change.group, change.key, default, change.value)
end
end
- defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
- if String.contains?(pattern, delimiter) do
- find_valid_delimiter(others, pattern, regex_delimiter)
- else
- {:ok, {delimiter, delimiter}}
- end
- end
+ @spec groups_without_keys() :: [atom()]
+ def groups_without_keys, do: @groups_without_keys
- @spec module_name?(String.t()) :: boolean()
- def module_name?(string) do
- Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
- string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
- end
+ @spec pleroma_not_keyword_values() :: [atom()]
+ def pleroma_not_keyword_values, do: @pleroma_not_keyword_values
end