diff options
Diffstat (limited to 'lib')
29 files changed, 576 insertions, 122 deletions
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex new file mode 100644 index 000000000..1fe03088d --- /dev/null +++ b/lib/mix/tasks/pleroma/config.ex @@ -0,0 +1,68 @@ +defmodule Mix.Tasks.Pleroma.Config do + use Mix.Task + alias Mix.Tasks.Pleroma.Common + alias Pleroma.Repo + alias Pleroma.Web.AdminAPI.Config + @shortdoc "Manages the location of the config" + @moduledoc """ + Manages the location of the config. + + ## Transfers config from file to DB. + + mix pleroma.config migrate_to_db + + ## Transfers config from DB to file. + + mix pleroma.config migrate_from_db ENV + """ + + def run(["migrate_to_db"]) do + Common.start_pleroma() + + if Pleroma.Config.get([:instance, :dynamic_configuration]) do + Application.get_all_env(:pleroma) + |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) + |> Enum.each(fn {k, v} -> + key = to_string(k) |> String.replace("Elixir.", "") + {:ok, _} = Config.update_or_create(%{key: key, value: v}) + Mix.shell().info("#{key} is migrated.") + end) + + Mix.shell().info("Settings migrated.") + else + Mix.shell().info( + "Migration is not allowed by config. You can change this behavior in instance settings." + ) + end + end + + def run(["migrate_from_db", env]) do + Common.start_pleroma() + + if Pleroma.Config.get([:instance, :dynamic_configuration]) do + config_path = "config/#{env}.migrated.secret.exs" + + {:ok, file} = File.open(config_path, [:write]) + + Repo.all(Config) + |> Enum.each(fn config -> + mark = if String.starts_with?(config.key, "Pleroma."), do: ",", else: ":" + + IO.write( + file, + "config :pleroma, #{config.key}#{mark} #{inspect(Config.from_binary(config.value))}\r\n" + ) + + {:ok, _} = Repo.delete(config) + Mix.shell().info("#{config.key} deleted from DB.") + end) + + File.close(file) + System.cmd("mix", ["format", config_path]) + else + Mix.shell().info( + "Migration is not allowed by config. You can change this behavior in instance settings." + ) + end + end +end diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index d2ddf450a..c2225af7d 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -55,15 +55,13 @@ defmodule Mix.Tasks.Pleroma.Emoji do are extracted). """ - @default_manifest Pleroma.Config.get!([:emoji, :default_manifest]) - def run(["ls-packs" | args]) do Application.ensure_all_started(:hackney) {options, [], []} = parse_global_opts(args) manifest = - fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest) + fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest()) Enum.each(manifest, fn {name, info} -> to_print = [ @@ -88,7 +86,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do {options, pack_names, []} = parse_global_opts(args) - manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest + manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest() manifest = fetch_manifest(manifest_url) @@ -298,4 +296,6 @@ defmodule Mix.Tasks.Pleroma.Emoji do Tesla.client(middleware) end + + defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest]) end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 88925dbaf..44e49cb69 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -30,6 +30,7 @@ defmodule Mix.Tasks.Pleroma.Instance do - `--dbuser DBUSER` - the user (aka role) to use for the database connection - `--dbpass DBPASS` - the password to use for the database connection - `--indexable Y/N` - Allow/disallow indexing site by search engines + - `--db-configurable Y/N` - Allow/disallow configuring instance from admin part """ def run(["gen" | rest]) do @@ -48,7 +49,8 @@ defmodule Mix.Tasks.Pleroma.Instance do dbname: :string, dbuser: :string, dbpass: :string, - indexable: :string + indexable: :string, + db_configurable: :string ], aliases: [ o: :output, @@ -101,6 +103,14 @@ defmodule Mix.Tasks.Pleroma.Instance do "y" ) === "y" + db_configurable? = + Common.get_option( + options, + :db_configurable, + "Do you want to be able to configure instance from admin part? (y/n)", + "y" + ) === "y" + dbhost = Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") @@ -144,7 +154,8 @@ defmodule Mix.Tasks.Pleroma.Instance do secret: secret, signing_salt: signing_salt, web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), - web_push_private_key: Base.url_encode64(web_push_private_key, padding: false) + web_push_private_key: Base.url_encode64(web_push_private_key, padding: false), + db_configurable?: db_configurable? ) result_psql = diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex index 52bd57cb7..73d9217be 100644 --- a/lib/mix/tasks/pleroma/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -16,7 +16,8 @@ config :pleroma, :instance, notify_email: "<%= notify_email %>", limit: 5000, registrations_open: true, - dedupe_media: false + dedupe_media: false, + dynamic_configuration: <%= db_configurable? %> config :pleroma, :media_proxy, enabled: false, diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 5627d20af..ba4cf8486 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -31,6 +31,7 @@ defmodule Pleroma.Application do [ # Start the Ecto repository %{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor}, + %{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}}, %{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}}, %{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}}, %{ @@ -174,7 +175,6 @@ defmodule Pleroma.Application do Pleroma.Repo.Instrumenter.setup() end - Prometheus.Registry.register_collector(:prometheus_process_collector) Pleroma.Web.Endpoint.MetricsExporter.setup() Pleroma.Web.Endpoint.PipelineInstrumenter.setup() Pleroma.Web.Endpoint.Instrumenter.setup() @@ -187,7 +187,7 @@ defmodule Pleroma.Application do else [] end ++ - if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do + if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do [:upload] else [] diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex new file mode 100644 index 000000000..0d6ece807 --- /dev/null +++ b/lib/pleroma/config/transfer_task.ex @@ -0,0 +1,41 @@ +defmodule Pleroma.Config.TransferTask do + use Task + alias Pleroma.Web.AdminAPI.Config + + def start_link do + load_and_update_env() + if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo) + :ignore + end + + def load_and_update_env do + if Pleroma.Config.get([:instance, :dynamic_configuration]) do + Pleroma.Repo.all(Config) + |> Enum.each(&update_env(&1)) + end + end + + defp update_env(setting) do + try do + key = + if String.starts_with?(setting.key, "Pleroma.") do + "Elixir." <> setting.key + else + setting.key + end + + Application.put_env( + :pleroma, + String.to_existing_atom(key), + Config.from_binary(setting.value) + ) + rescue + e -> + require Logger + + Logger.warn( + "updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}" + ) + end + end +end diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index b77b26f7f..854d46b1a 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -22,7 +22,6 @@ defmodule Pleroma.Emoji do @ets __MODULE__.Ets @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] - @groups Pleroma.Config.get([:emoji, :groups]) @doc false def start_link do @@ -87,6 +86,8 @@ defmodule Pleroma.Emoji do "emoji" ) + emoji_groups = Pleroma.Config.get([:emoji, :groups]) + case File.ls(emoji_dir_path) do {:error, :enoent} -> # The custom emoji directory doesn't exist, @@ -118,7 +119,7 @@ defmodule Pleroma.Emoji do emojis = Enum.flat_map( packs, - fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end + fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end ) true = :ets.insert(@ets, emojis) @@ -129,9 +130,9 @@ defmodule Pleroma.Emoji do shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], []) emojis = - (load_from_file("config/emoji.txt") ++ - load_from_file("config/custom_emoji.txt") ++ - load_from_globs(shortcode_globs)) + (load_from_file("config/emoji.txt", emoji_groups) ++ + load_from_file("config/custom_emoji.txt", emoji_groups) ++ + load_from_globs(shortcode_globs, emoji_groups)) |> Enum.reject(fn value -> value == nil end) true = :ets.insert(@ets, emojis) @@ -139,13 +140,13 @@ defmodule Pleroma.Emoji do :ok end - defp load_pack(pack_dir) do + defp load_pack(pack_dir, emoji_groups) do pack_name = Path.basename(pack_dir) emoji_txt = Path.join(pack_dir, "emoji.txt") if File.exists?(emoji_txt) do - load_from_file(emoji_txt) + load_from_file(emoji_txt, emoji_groups) else Logger.info( "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji" @@ -155,7 +156,7 @@ defmodule Pleroma.Emoji do |> Enum.map(fn {shortcode, rel_file} -> filename = Path.join("/emoji/#{pack_name}", rel_file) - {shortcode, filename, [to_string(match_extra(@groups, filename))]} + {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]} end) end end @@ -184,21 +185,21 @@ defmodule Pleroma.Emoji do |> Enum.filter(fn f -> Path.extname(f) in exts end) end - defp load_from_file(file) do + defp load_from_file(file, emoji_groups) do if File.exists?(file) do - load_from_file_stream(File.stream!(file)) + load_from_file_stream(File.stream!(file), emoji_groups) else [] end end - defp load_from_file_stream(stream) do + defp load_from_file_stream(stream, emoji_groups) do stream |> Stream.map(&String.trim/1) |> Stream.map(fn line -> case String.split(line, ~r/,\s*/) do [name, file] -> - {name, file, [to_string(match_extra(@groups, file))]} + {name, file, [to_string(match_extra(emoji_groups, file))]} [name, file | tags] -> {name, file, tags} @@ -210,7 +211,7 @@ defmodule Pleroma.Emoji do |> Enum.to_list() end - defp load_from_globs(globs) do + defp load_from_globs(globs, emoji_groups) do static_path = Path.join(:code.priv_dir(:pleroma), "static") paths = @@ -221,7 +222,7 @@ defmodule Pleroma.Emoji do |> Enum.concat() Enum.map(paths, fn path -> - tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path))) + tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path))) shortcode = Path.basename(path, Path.extname(path)) external_path = Path.join("/", Path.relative_to(path, static_path)) {shortcode, external_path, [to_string(tag)]} diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index e5e78ee4f..8c226c944 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -89,7 +89,7 @@ defmodule Pleroma.HTML do Cachex.fetch!(:scrubber_cache, key, fn _key -> result = content - |> Floki.filter_out("a.mention") + |> Floki.filter_out("a.mention,a.hashtag") |> Floki.attribute("a", "href") |> Enum.at(0) diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index 5e107f4c9..fa5043bc5 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Instances do def reachability_datetime_threshold do federation_reachability_timeout_days = - Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0 + Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0) if federation_reachability_timeout_days > 0 do NaiveDateTime.add( diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 2f4687fa2..ada9da0bb 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Object.Containment do @moduledoc """ This module contains some useful functions for containing objects to specific diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index ca980c629..c422490ac 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -85,6 +85,9 @@ defmodule Pleroma.Object.Fetcher do :ok <- Containment.contain_origin_from_id(id, data) do {:ok, data} else + {:ok, %{status: code}} when code in [404, 410] -> + {:error, "Object has been deleted"} + e -> {:error, e} end diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex index e02ba4213..9ba5875fa 100644 --- a/lib/pleroma/plugs/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter.ex @@ -14,13 +14,20 @@ defmodule Pleroma.Plugs.RateLimiter do It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated. + To disable a limiter set its value to `nil`. + ### Example config :pleroma, :rate_limit, one: {1000, 10}, - two: [{10_000, 10}, {10_000, 50}] + two: [{10_000, 10}, {10_000, 50}], + foobar: nil + + Here we have three limiters: - Here we have two limiters: `one` which is not over 10req/1s and `two` which has two limits 10req/10s for unauthenticated users and 50req/10s for authenticated users. + * `one` which is not over 10req/1s + * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users + * `foobar` which is disabled ## Usage diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex index fd77b8d8f..8d0fac7ee 100644 --- a/lib/pleroma/plugs/uploaded_media.ex +++ b/lib/pleroma/plugs/uploaded_media.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Plugs.UploadedMedia do conn end - config = Pleroma.Config.get([Pleroma.Upload]) + config = Pleroma.Config.get(Pleroma.Upload) with uploader <- Keyword.fetch!(config, :uploader), proxy_remote = Keyword.get(config, :proxy_remote, false), diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index 285d57309..de0f6e1bc 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -146,7 +146,7 @@ defmodule Pleroma.ReverseProxy do Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") method = method |> String.downcase() |> String.to_existing_atom() - case :hackney.request(method, url, headers, "", hackney_opts) do + case hackney().request(method, url, headers, "", hackney_opts) do {:ok, code, headers, client} when code in @valid_resp_codes -> {:ok, code, downcase_headers(headers), client} @@ -196,7 +196,7 @@ defmodule Pleroma.ReverseProxy do duration, Keyword.get(opts, :max_read_duration, @max_read_duration) ), - {:ok, data} <- :hackney.stream_body(client), + {:ok, data} <- hackney().stream_body(client), {:ok, duration} <- increase_read_duration(duration), sent_so_far = sent_so_far + byte_size(data), :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), @@ -377,4 +377,6 @@ defmodule Pleroma.ReverseProxy do defp increase_read_duration(_) do {:ok, :no_duration_limit, :no_duration_limit} end + + defp hackney, do: Pleroma.Config.get(:hackney, :hackney) end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 9449a88d0..3a9ae8d73 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1036,9 +1036,7 @@ defmodule Pleroma.User do Pleroma.HTML.Scrubber.TwitterText end - @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy]) - - def html_filter_policy(_), do: @default_scrubbers + def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) def fetch_by_ap_id(ap_id) do ap_try = ActivityPub.make_user_from_ap_id(ap_id) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index f88dffa7b..ed06c2ab9 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -7,45 +7,69 @@ defmodule Pleroma.User.Search do alias Pleroma.User import Ecto.Query - def search(query, opts \\ []) do + @similarity_threshold 0.25 + @limit 20 + + def search(query_string, opts \\ []) do resolve = Keyword.get(opts, :resolve, false) + following = Keyword.get(opts, :following, false) + result_limit = Keyword.get(opts, :limit, @limit) + offset = Keyword.get(opts, :offset, 0) + for_user = Keyword.get(opts, :for_user) # Strip the beginning @ off if there is a query - query = String.trim_leading(query, "@") + query_string = String.trim_leading(query_string, "@") - maybe_resolve(resolve, for_user, query) + maybe_resolve(resolve, for_user, query_string) {:ok, results} = Repo.transaction(fn -> - Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) + Ecto.Adapters.SQL.query( + Repo, + "select set_limit(#{@similarity_threshold})", + [] + ) - query - |> search_query(for_user) + query_string + |> search_query(for_user, following) + |> paginate(result_limit, offset) |> Repo.all() end) results end - defp search_query(query, for_user) do - query - |> union_query() + defp search_query(query_string, for_user, following) do + for_user + |> base_query(following) + |> search_subqueries(query_string) + |> union_subqueries |> distinct_query() |> boost_search_rank_query(for_user) |> subquery() |> order_by(desc: :search_rank) - |> limit(20) |> maybe_restrict_local(for_user) end - defp union_query(query) do - fts_subquery = fts_search_subquery(query) - trigram_subquery = trigram_search_subquery(query) + defp base_query(_user, false), do: User + defp base_query(user, true), do: User.get_followers_query(user) + + defp paginate(query, limit, offset) do + from(q in query, limit: ^limit, offset: ^offset) + end + defp union_subqueries({fts_subquery, trigram_subquery}) do from(s in trigram_subquery, union_all: ^fts_subquery) end + defp search_subqueries(base_query, query_string) do + { + fts_search_subquery(base_query, query_string), + trigram_search_subquery(base_query, query_string) + } + end + defp distinct_query(q) do from(s in subquery(q), order_by: s.search_type, distinct: s.id) end @@ -102,7 +126,8 @@ defmodule Pleroma.User.Search do ) end - defp fts_search_subquery(term, query \\ User) do + @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() + defp fts_search_subquery(query, term) do processed_query = term |> String.replace(~r/\W+/, " ") @@ -144,9 +169,10 @@ defmodule Pleroma.User.Search do |> User.restrict_deactivated() end - defp trigram_search_subquery(term) do + @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() + defp trigram_search_subquery(query, term) do from( - u in User, + u in query, select_merge: %{ # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason search_type: fragment("?", 1), diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 8f1399ce6..a05e03263 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do true else inbox_info = URI.parse(inbox) - !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) + !Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host) end end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de2a13c01..03dfdca82 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.AdminAPI.Config + alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI @@ -362,6 +364,41 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + def config_show(conn, _params) do + configs = Pleroma.Repo.all(Config) + + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: configs}) + end + + def config_update(conn, %{"configs" => configs}) do + updated = + if Pleroma.Config.get([:instance, :dynamic_configuration]) do + updated = + Enum.map(configs, fn + %{"key" => key, "value" => value} -> + {:ok, config} = Config.update_or_create(%{key: key, value: value}) + config + + %{"key" => key, "delete" => "true"} -> + {:ok, _} = Config.delete(key) + nil + end) + |> Enum.reject(&is_nil(&1)) + + Pleroma.Config.TransferTask.load_and_update_env() + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env)]) + updated + else + [] + end + + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: updated}) + end + def errors(conn, {:error, :not_found}) do conn |> put_status(404) diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex new file mode 100644 index 000000000..b7072f050 --- /dev/null +++ b/lib/pleroma/web/admin_api/config.ex @@ -0,0 +1,144 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.Config do + use Ecto.Schema + import Ecto.Changeset + alias __MODULE__ + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + schema "config" do + field(:key, :string) + field(:value, :binary) + + timestamps() + end + + @spec get_by_key(String.t()) :: Config.t() | nil + def get_by_key(key), do: Repo.get_by(Config, key: key) + + @spec changeset(Config.t(), map()) :: Changeset.t() + def changeset(config, params \\ %{}) do + config + |> cast(params, [:key, :value]) + |> validate_required([:key, :value]) + |> unique_constraint(:key) + end + + @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def create(%{key: key, value: value}) do + %Config{} + |> changeset(%{key: key, value: transform(value)}) + |> Repo.insert() + end + + @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()} + def update(%Config{} = config, %{value: value}) do + config + |> change(value: transform(value)) + |> Repo.update() + end + + @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def update_or_create(%{key: key} = params) do + with %Config{} = config <- Config.get_by_key(key) do + Config.update(config, params) + else + nil -> Config.create(params) + end + end + + @spec delete(String.t()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def delete(key) do + with %Config{} = config <- Config.get_by_key(key) do + Repo.delete(config) + else + nil -> {:error, "Config with key #{key} not found"} + end + end + + @spec from_binary(binary()) :: term() + def from_binary(value), do: :erlang.binary_to_term(value) + + @spec from_binary_to_map(binary()) :: any() + def from_binary_to_map(binary) do + from_binary(binary) + |> do_convert() + end + + defp do_convert([{k, v}] = value) when is_list(value) and length(value) == 1, + do: %{k => do_convert(v)} + + defp do_convert(values) when is_list(values), do: for(val <- values, do: do_convert(val)) + + defp do_convert({k, v} = value) when is_tuple(value), + do: %{k => do_convert(v)} + + defp do_convert(value) when is_binary(value) or is_atom(value) or is_map(value), + do: value + + @spec transform(any()) :: binary() + def transform(entity) when is_map(entity) do + tuples = + for {k, v} <- entity, + into: [], + do: {if(is_atom(k), do: k, else: String.to_atom(k)), do_transform(v)} + + Enum.reject(tuples, fn {_k, v} -> is_nil(v) end) + |> Enum.sort() + |> :erlang.term_to_binary() + end + + def transform(entity) when is_list(entity) do + list = Enum.map(entity, &do_transform(&1)) + :erlang.term_to_binary(list) + end + + def transform(entity), do: :erlang.term_to_binary(entity) + + defp do_transform(%Regex{} = value) when is_map(value), do: value + + defp do_transform(value) when is_map(value) do + values = + for {key, val} <- value, + into: [], + do: {String.to_atom(key), do_transform(val)} + + Enum.sort(values) + end + + defp do_transform(value) when is_list(value) do + Enum.map(value, &do_transform(&1)) + end + + defp do_transform(entity) when is_list(entity) and length(entity) == 1, do: hd(entity) + + defp do_transform(value) when is_binary(value) do + value = String.trim(value) + + case String.length(value) do + 0 -> + nil + + _ -> + cond do + String.starts_with?(value, "Pleroma") -> + String.to_existing_atom("Elixir." <> value) + + String.starts_with?(value, ":") -> + String.replace(value, ":", "") |> String.to_existing_atom() + + String.starts_with?(value, "i:") -> + String.replace(value, "i:", "") |> String.to_integer() + + true -> + value + end + end + end + + defp do_transform(value), do: value +end diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex new file mode 100644 index 000000000..c8560033e --- /dev/null +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -0,0 +1,16 @@ +defmodule Pleroma.Web.AdminAPI.ConfigView do + use Pleroma.Web, :view + + def render("index.json", %{configs: configs}) do + %{ + configs: render_many(configs, __MODULE__, "show.json", as: :config) + } + end + + def render("show.json", %{config: config}) do + %{ + key: config.key, + value: Pleroma.Web.AdminAPI.Config.from_binary_to_map(config.value) + } + end +end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 55706eeb8..8a753bb4f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do |> put_status(status) |> json(json) end + + @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil + def fetch_integer_param(params, name, default \\ nil) do + params + |> Map.get(name, default) + |> param_to_integer(default) + end + + defp param_to_integer(val, _) when is_integer(val), do: val + + defp param_to_integer(val, default) when is_binary(val) do + case Integer.parse(val) do + {res, _} -> res + _ -> default + end + end + + defp param_to_integer(_, default), do: default end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index bd76e4295..ddaf88f1d 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -91,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do Plug.Session, store: :cookie, key: cookie_name, - signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, + signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"), http_only: true, secure: secure_cookies, extra: extra diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 46049dd24..eea4040ec 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -136,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do _ -> :error end end) + |> add_if_present(params, "pleroma_background_image", :background, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :background) do + {:ok, object.data} + else + _ -> :error + end + end) |> Map.put(:emoji, user_info_emojis) info_cng = User.Info.profile_update(user.info, info_params) @@ -160,8 +168,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def verify_credentials(%{assigns: %{user: user}} = conn, _) do + chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + account = - AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + AccountView.render("account.json", %{ + user: user, + for: user, + with_pleroma_settings: true, + with_chat_token: chat_token + }) json(conn, account) end @@ -1118,58 +1133,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - statuses = Activity.search(user, query) - tags_path = Web.base_url() <> "/tag/" - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - statuses = Activity.search(user, query) - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - - json(conn, res) - end - def favourites(%{assigns: %{user: user}} = conn, params) do params = params diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex new file mode 100644 index 000000000..0d1e2355d --- /dev/null +++ b/lib/pleroma/web/mastodon_api/search_controller.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchController do + use Pleroma.Web, :controller + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + alias Pleroma.Web.ControllerHelper + + require Logger + + plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search]) + + def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, search_options(params, user)) + statuses = Activity.search(user, query) + tags_path = Web.base_url() <> "/tag/" + + tags = + query + |> String.split() + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) + |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) + + res = %{ + "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), + "statuses" => + StatusView.render("index.json", activities: statuses, for: user, as: :activity), + "hashtags" => tags + } + + json(conn, res) + end + + def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, search_options(params, user)) + statuses = Activity.search(user, query) + + tags = + query + |> String.split() + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) + + res = %{ + "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), + "statuses" => + StatusView.render("index.json", activities: statuses, for: user, as: :activity), + "hashtags" => tags + } + + json(conn, res) + end + + def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, search_options(params, user)) + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + + json(conn, res) + end + + defp search_options(params, user) do + [ + resolve: params["resolve"] == "true", + following: params["following"] == "true", + limit: ControllerHelper.fetch_integer_param(params, "limit"), + offset: ControllerHelper.fetch_integer_param(params, "offset"), + for_user: user + ] + end +end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b91726b45..72ae9bcda 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -125,13 +125,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do hide_follows: user.info.hide_follows, hide_favorites: user.info.hide_favorites, relationship: relationship, - skip_thread_containment: user.info.skip_thread_containment + skip_thread_containment: user.info.skip_thread_containment, + background_image: image_url(user.info.background) |> MediaProxy.url() } } |> maybe_put_role(user, opts[:for]) |> maybe_put_settings(user, opts[:for], user_info) |> maybe_put_notification_settings(user, opts[:for]) |> maybe_put_settings_store(user, opts[:for], opts) + |> maybe_put_chat_token(user, opts[:for], opts) end defp username_from_nickname(string) when is_binary(string) do @@ -163,6 +165,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_settings_store(data, _, _, _), do: data + defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{ + with_chat_token: token + }) do + data + |> Kernel.put_in([:pleroma, :chat_token], token) + end + + defp maybe_put_chat_token(data, _, _, _), do: data + defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do data |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) @@ -182,4 +193,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end defp maybe_put_notification_settings(data, _, _), do: data + + defp image_url(%{"url" => [%{"href" => href} | _]}), do: href + defp image_url(_), do: nil end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f412f7eb2..90c304487 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.OAuth.Token do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Query - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) @type t :: %__MODULE__{} schema "oauth_tokens" do @@ -78,7 +77,7 @@ defmodule Pleroma.Web.OAuth.Token do defp put_valid_until(changeset, attrs) do expires_in = - Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) changeset |> change(%{valid_until: expires_in}) @@ -123,4 +122,6 @@ defmodule Pleroma.Web.OAuth.Token do end def is_expired?(_), do: false + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex index 64e78b183..2648571ad 100644 --- a/lib/pleroma/web/oauth/token/response.ex +++ b/lib/pleroma/web/oauth/token/response.ex @@ -4,15 +4,13 @@ defmodule Pleroma.Web.OAuth.Token.Response do alias Pleroma.User alias Pleroma.Web.OAuth.Token.Utils - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) - @doc false def build(%User{} = user, token, opts \\ %{}) do %{ token_type: "Bearer", access_token: token.token, refresh_token: token.refresh_token, - expires_in: @expires_in, + expires_in: expires_in(), scope: Enum.join(token.scopes, " "), me: user.ap_id } @@ -25,8 +23,10 @@ defmodule Pleroma.Web.OAuth.Token.Response do access_token: token.token, refresh_token: token.refresh_token, created_at: Utils.format_created_at(token), - expires_in: @expires_in, + expires_in: expires_in(), scope: Enum.join(token.scopes, " ") } end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index 4a7c5eae0..82f1cce29 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -1,15 +1,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do - with elements = [_ | _] <- get_elements(html, key_name, prefix), - meta_data = - Enum.reduce(elements, data, fn el, acc -> - attributes = normalize_attributes(el, prefix, key_name, value_name) + meta_data = + html + |> get_elements(key_name, prefix) + |> Enum.reduce(data, fn el, acc -> + attributes = normalize_attributes(el, prefix, key_name, value_name) - Map.merge(acc, attributes) - end) do - {:ok, meta_data} + Map.merge(acc, attributes) + end) + |> maybe_put_title(html) + + if Enum.empty?(meta_data) do + {:error, error_message} else - _e -> {:error, error_message} + {:ok, meta_data} end end @@ -27,4 +31,17 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do %{String.to_atom(data[key_name]) => data[value_name]} end + + defp maybe_put_title(%{title: _} = meta, _), do: meta + + defp maybe_put_title(meta, html) do + case get_page_title(html) do + "" -> meta + title -> Map.put_new(meta, :title, title) + end + end + + defp get_page_title(html) do + Floki.find(html, "title") |> Floki.text() + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 1b37d6a93..0e3f73226 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -202,6 +202,9 @@ defmodule Pleroma.Web.Router do put("/statuses/:id", AdminAPIController, :status_update) delete("/statuses/:id", AdminAPIController, :status_delete) + + get("/config", AdminAPIController, :config_show) + post("/config", AdminAPIController, :config_update) end scope "/", Pleroma.Web.TwitterAPI do @@ -412,7 +415,7 @@ defmodule Pleroma.Web.Router do get("/trends", MastodonAPIController, :empty_array) - get("/accounts/search", MastodonAPIController, :account_search) + get("/accounts/search", SearchController, :account_search) scope [] do pipe_through(:oauth_read_or_public) @@ -431,7 +434,7 @@ defmodule Pleroma.Web.Router do get("/accounts/:id/following", MastodonAPIController, :following) get("/accounts/:id", MastodonAPIController, :user) - get("/search", MastodonAPIController, :search) + get("/search", SearchController, :search) get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) end @@ -439,7 +442,7 @@ defmodule Pleroma.Web.Router do scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through([:api, :oauth_read_or_public]) - get("/search", MastodonAPIController, :search2) + get("/search", SearchController, :search2) end scope "/api", Pleroma.Web do |