diff options
Diffstat (limited to 'lib/pleroma')
82 files changed, 1318 insertions, 372 deletions
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index cb15dc1e9..3f199c002 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -94,6 +94,7 @@ defmodule Pleroma.Application do children = [ Pleroma.PromEx, + Pleroma.LDAP, Pleroma.Repo, Config.TransferTask, Pleroma.Emoji, diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index ffc95f144..140dd7711 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -22,7 +22,8 @@ defmodule Pleroma.Config.TransferTask do {:pleroma, :markup}, {:pleroma, :streamer}, {:pleroma, :pools}, - {:pleroma, :connections_pool} + {:pleroma, :connections_pool}, + {:pleroma, :ldap} ] defp reboot_time_subkeys, diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index a018e91fc..2d08cd7a1 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -87,6 +87,41 @@ defmodule Pleroma.Constants do ] ) + const(activity_types, + do: [ + "Block", + "Create", + "Update", + "Delete", + "Follow", + "Accept", + "Reject", + "Add", + "Remove", + "Like", + "Announce", + "Undo", + "Flag", + "EmojiReact" + ] + ) + + const(allowed_activity_types_from_strangers, + do: [ + "Block", + "Create", + "Flag", + "Follow", + "Like", + "EmojiReact", + "Announce" + ] + ) + + const(object_types, + do: ~w[Event Question Answer Audio Video Image Article Note Page ChatMessage] + ) + # basic regex, just there to weed out potential mistakes # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 const(mime_regex, diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index 816499917..a4f427ae5 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -74,11 +74,14 @@ defmodule Pleroma.Frontend do new_file_path = Path.join(dest, path) - new_file_path + path |> Path.dirname() + |> then(&Path.join(dest, &1)) |> File.mkdir_p!() - File.write!(new_file_path, data) + if not File.dir?(new_file_path) do + File.write!(new_file_path, data) + end end) end end diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index dcb27a29d..32c1080f7 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -52,6 +52,7 @@ defmodule Pleroma.HTTP.AdapterHelper do case adapter() do Tesla.Adapter.Gun -> AdapterHelper.Gun Tesla.Adapter.Hackney -> AdapterHelper.Hackney + {Tesla.Adapter.Finch, _} -> AdapterHelper.Finch _ -> AdapterHelper.Default end end @@ -118,4 +119,13 @@ defmodule Pleroma.HTTP.AdapterHelper do host_charlist end end + + @spec can_stream? :: bool() + def can_stream? do + case Application.get_env(:tesla, :adapter) do + Tesla.Adapter.Gun -> true + {Tesla.Adapter.Finch, _} -> true + _ -> false + end + end end diff --git a/lib/pleroma/http/adapter_helper/finch.ex b/lib/pleroma/http/adapter_helper/finch.ex new file mode 100644 index 000000000..181caed7e --- /dev/null +++ b/lib/pleroma/http/adapter_helper/finch.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.AdapterHelper.Finch do + @behaviour Pleroma.HTTP.AdapterHelper + + alias Pleroma.Config + alias Pleroma.HTTP.AdapterHelper + + @spec options(keyword(), URI.t()) :: keyword() + def options(incoming_opts \\ [], %URI{} = _uri) do + proxy = + [:http, :proxy_url] + |> Config.get() + |> AdapterHelper.format_proxy() + + config_opts = Config.get([:http, :adapter], []) + + config_opts + |> Keyword.merge(incoming_opts) + |> AdapterHelper.maybe_add_proxy(proxy) + |> maybe_stream() + end + + # Finch uses [response: :stream] + defp maybe_stream(opts) do + case Keyword.pop(opts, :stream, nil) do + {true, opts} -> Keyword.put(opts, :response, :stream) + {_, opts} -> opts + end + end +end diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 1fe8dd4b2..30ba26765 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -32,6 +32,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do |> AdapterHelper.maybe_add_proxy(proxy) |> Keyword.merge(incoming_opts) |> put_timeout() + |> maybe_stream() end defp add_scheme_opts(opts, %{scheme: "http"}), do: opts @@ -47,6 +48,14 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do Keyword.put(opts, :timeout, recv_timeout) end + # Gun uses [body_as: :stream] + defp maybe_stream(opts) do + case Keyword.pop(opts, :stream, nil) do + {true, opts} -> Keyword.put(opts, :body_as, :stream) + {_, opts} -> opts + end + end + @spec pool_timeout(pool()) :: non_neg_integer() def pool_timeout(pool) do default = Config.get([:pools, :default, :recv_timeout], 5_000) diff --git a/lib/pleroma/ldap.ex b/lib/pleroma/ldap.ex new file mode 100644 index 000000000..b591c2918 --- /dev/null +++ b/lib/pleroma/ldap.ex @@ -0,0 +1,271 @@ +defmodule Pleroma.LDAP do + use GenServer + + require Logger + + alias Pleroma.Config + alias Pleroma.User + + import Pleroma.Web.Auth.Helpers, only: [fetch_user: 1] + + @connection_timeout 2_000 + @search_timeout 2_000 + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def bind_user(name, password) do + GenServer.call(__MODULE__, {:bind_user, name, password}) + end + + def change_password(name, password, new_password) do + GenServer.call(__MODULE__, {:change_password, name, password, new_password}) + end + + @impl true + def init(state) do + case {Config.get(Pleroma.Web.Auth.Authenticator), Config.get([:ldap, :enabled])} do + {Pleroma.Web.Auth.LDAPAuthenticator, true} -> + {:ok, state, {:continue, :connect}} + + {Pleroma.Web.Auth.LDAPAuthenticator, false} -> + Logger.error( + "LDAP Authenticator enabled but :pleroma, :ldap is not enabled. Auth will not work." + ) + + {:ok, state} + + {_, true} -> + Logger.warning( + ":pleroma, :ldap is enabled but Pleroma.Web.Authenticator is not set to the LDAPAuthenticator. LDAP will not be used." + ) + + {:ok, state} + + _ -> + {:ok, state} + end + end + + @impl true + def handle_continue(:connect, _state), do: do_handle_connect() + + @impl true + def handle_info(:connect, _state), do: do_handle_connect() + + def handle_info({:bind_after_reconnect, name, password, from}, state) do + result = do_bind_user(state[:handle], name, password) + + GenServer.reply(from, result) + + {:noreply, state} + end + + @impl true + def handle_call({:bind_user, name, password}, from, state) do + case do_bind_user(state[:handle], name, password) do + :needs_reconnect -> + Process.send(self(), {:bind_after_reconnect, name, password, from}, []) + {:noreply, state, {:continue, :connect}} + + result -> + {:reply, result, state, :hibernate} + end + end + + def handle_call({:change_password, name, password, new_password}, _from, state) do + result = change_password(state[:handle], name, password, new_password) + + {:reply, result, state, :hibernate} + end + + @impl true + def terminate(_, state) do + handle = Keyword.get(state, :handle) + + if not is_nil(handle) do + :eldap.close(handle) + end + + :ok + end + + defp do_handle_connect do + state = + case connect() do + {:ok, handle} -> + :eldap.controlling_process(handle, self()) + Process.link(handle) + [handle: handle] + + _ -> + Logger.error("Failed to connect to LDAP. Retrying in 5000ms") + Process.send_after(self(), :connect, 5_000) + [] + end + + {:noreply, state} + end + + defp connect do + ldap = Config.get(:ldap, []) + host = Keyword.get(ldap, :host, "localhost") + port = Keyword.get(ldap, :port, 389) + ssl = Keyword.get(ldap, :ssl, false) + tls = Keyword.get(ldap, :tls, false) + cacertfile = Keyword.get(ldap, :cacertfile) || CAStore.file_path() + + if ssl, do: Application.ensure_all_started(:ssl) + + default_secure_opts = [ + verify: :verify_peer, + cacerts: decode_certfile(cacertfile), + customize_hostname_check: [ + fqdn_fun: fn _ -> to_charlist(host) end + ] + ] + + sslopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :sslopts, [])) + tlsopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :tlsopts, [])) + + default_options = [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] + + # :sslopts can only be included in :eldap.open/2 when {ssl: true} + # or the connection will fail + options = + if ssl do + default_options ++ [{:sslopts, sslopts}] + else + default_options + end + + case :eldap.open([to_charlist(host)], options) do + {:ok, handle} -> + try do + cond do + tls -> + case :eldap.start_tls( + handle, + tlsopts, + @connection_timeout + ) do + :ok -> + {:ok, handle} + + error -> + Logger.error("Could not start TLS: #{inspect(error)}") + :eldap.close(handle) + end + + true -> + {:ok, handle} + end + after + :ok + end + + {:error, error} -> + Logger.error("Could not open LDAP connection: #{inspect(error)}") + {:error, {:ldap_connection_error, error}} + end + end + + defp do_bind_user(handle, name, password) do + dn = make_dn(name) + + case :eldap.simple_bind(handle, dn, password) do + :ok -> + case fetch_user(name) do + %User{} = user -> + user + + _ -> + register_user(handle, ldap_base(), ldap_uid(), name) + end + + # eldap does not inform us of socket closure + # until it is used + {:error, {:gen_tcp_error, :closed}} -> + :eldap.close(handle) + :needs_reconnect + + {:error, error} = e -> + Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}") + e + end + end + + defp register_user(handle, base, uid, name) do + case :eldap.search(handle, [ + {:base, to_charlist(base)}, + {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, + {:scope, :eldap.wholeSubtree()}, + {:timeout, @search_timeout} + ]) do + # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field + # https://github.com/erlang/otp/pull/5538 + {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} -> + try_register(name, attributes) + + {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} -> + try_register(name, attributes) + + error -> + Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}") + {:error, {:ldap_search_error, error}} + end + end + + defp try_register(name, attributes) do + mail_attribute = Config.get([:ldap, :mail]) + + params = %{ + name: name, + nickname: name, + password: nil + } + + params = + case List.keyfind(attributes, to_charlist(mail_attribute), 0) do + {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) + _ -> params + end + + changeset = User.register_changeset_ldap(%User{}, params) + + case User.register(changeset) do + {:ok, user} -> user + error -> error + end + end + + defp change_password(handle, name, password, new_password) do + dn = make_dn(name) + + with :ok <- :eldap.simple_bind(handle, dn, password) do + :eldap.modify_password(handle, dn, to_charlist(new_password), to_charlist(password)) + end + end + + defp decode_certfile(file) do + with {:ok, data} <- File.read(file) do + data + |> :public_key.pem_decode() + |> Enum.map(fn {_, b, _} -> b end) + else + _ -> + Logger.error("Unable to read certfile: #{file}") + [] + end + end + + defp ldap_uid, do: to_charlist(Config.get([:ldap, :uid], "cn")) + defp ldap_base, do: to_charlist(Config.get([:ldap, :base])) + + defp make_dn(name) do + uid = ldap_uid() + base = ldap_base() + ~c"#{uid}=#{name},#{base}" + end +end diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex index 5020a8ff8..1afbde484 100644 --- a/lib/pleroma/maps.ex +++ b/lib/pleroma/maps.ex @@ -20,15 +20,13 @@ defmodule Pleroma.Maps do end def filter_empty_values(data) do - # TODO: Change to Map.filter in Elixir 1.13+ data - |> Enum.filter(fn + |> Map.filter(fn {_k, nil} -> false {_k, ""} -> false {_k, []} -> false {_k, %{} = v} -> Map.keys(v) != [] {_k, _v} -> true end) - |> Map.new() end end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 748f18e6c..77dfda851 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -99,27 +99,6 @@ defmodule Pleroma.Object do def get_by_id(nil), do: nil def get_by_id(id), do: Repo.get(Object, id) - @spec get_by_id_and_maybe_refetch(integer(), list()) :: Object.t() | nil - def get_by_id_and_maybe_refetch(id, opts \\ []) do - with %Object{updated_at: updated_at} = object <- get_by_id(id) do - if opts[:interval] && - NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do - case Fetcher.refetch_object(object) do - {:ok, %Object{} = object} -> - object - - e -> - Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}") - object - end - else - object - end - else - nil -> nil - end - end - def get_by_ap_id(nil), do: nil def get_by_ap_id(ap_id) do diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 9d9a201ca..c85a8b09f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -58,8 +58,12 @@ defmodule Pleroma.Object.Fetcher do end end + @typep fetcher_errors :: + :error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier + # Note: will create a Create activity, which we need internally at the moment. - @spec fetch_object_from_id(String.t(), list()) :: {:ok, Object.t()} | {:error | :reject, any()} + @spec fetch_object_from_id(String.t(), list()) :: + {:ok, Object.t()} | {fetcher_errors(), any()} | Pipeline.errors() def fetch_object_from_id(id, options \\ []) do with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)}, {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])}, @@ -141,6 +145,7 @@ defmodule Pleroma.Object.Fetcher do Logger.debug("Fetching object #{id} via AP") with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, + {_, true} <- {:mrf, MRF.id_filter(id)}, {:ok, body} <- get_object(id), {:ok, data} <- safe_json_decode(body), :ok <- Containment.contain_origin_from_id(id, data) do @@ -156,6 +161,9 @@ defmodule Pleroma.Object.Fetcher do {:error, e} -> {:error, e} + {:mrf, false} -> + {:error, {:reject, "Filtered by id"}} + e -> {:error, e} end diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex index bcfcd1243..af2d35c8f 100644 --- a/lib/pleroma/release_tasks.ex +++ b/lib/pleroma/release_tasks.ex @@ -16,17 +16,24 @@ defmodule Pleroma.ReleaseTasks do end end + def find_module(task) do + module_name = + task + |> String.split(".") + |> Enum.map(&String.capitalize/1) + |> then(fn x -> [Mix, Tasks, Pleroma] ++ x end) + |> Module.concat() + + case Code.ensure_loaded(module_name) do + {:module, _} -> module_name + _ -> nil + end + end + defp mix_task(task, args) do Application.load(:pleroma) - {:ok, modules} = :application.get_key(:pleroma, :modules) - - module = - Enum.find(modules, fn module -> - module = Module.split(module) - match?(["Mix", "Tasks", "Pleroma" | _], module) and - String.downcase(List.last(module)) == task - end) + module = find_module(task) if module do module.run(args) diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex index 9bba5b30f..cafae8099 100644 --- a/lib/pleroma/search/meilisearch.ex +++ b/lib/pleroma/search/meilisearch.ex @@ -122,6 +122,7 @@ defmodule Pleroma.Search.Meilisearch do # Only index public or unlisted Notes if not is_nil(object) and object.data["type"] == "Note" and not is_nil(object.data["content"]) and + not is_nil(object.data["published"]) and (Pleroma.Constants.as_public() in object.data["to"] or Pleroma.Constants.as_public() in object.data["cc"]) and object.data["content"] not in ["", "."] do diff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex index 7ee643277..a8480bf36 100644 --- a/lib/pleroma/upload/filter/analyze_metadata.ex +++ b/lib/pleroma/upload/filter/analyze_metadata.ex @@ -90,9 +90,13 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadata do {:ok, rgb} = if Image.has_alpha?(resized_image) do # remove alpha channel - resized_image - |> Operation.extract_band!(0, n: 3) - |> Image.write_to_binary() + case Operation.extract_band(resized_image, 0, n: 3) do + {:ok, data} -> + Image.write_to_binary(data) + + _ -> + Image.write_to_binary(resized_image) + end else Image.write_to_binary(resized_image) end diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex index ef793d390..7b278d299 100644 --- a/lib/pleroma/upload/filter/dedupe.ex +++ b/lib/pleroma/upload/filter/dedupe.ex @@ -17,8 +17,16 @@ defmodule Pleroma.Upload.Filter.Dedupe do |> Base.encode16(case: :lower) filename = shasum <> "." <> extension - {:ok, :filtered, %Upload{upload | id: shasum, path: filename}} + + {:ok, :filtered, %Upload{upload | id: shasum, path: shard_path(filename)}} end def filter(_), do: {:ok, :noop} + + @spec shard_path(String.t()) :: String.t() + def shard_path( + <<a::binary-size(2), b::binary-size(2), c::binary-size(2), _::binary>> = filename + ) do + Path.join([a, b, c, filename]) + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c6c536943..7a36ece77 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -419,6 +419,11 @@ defmodule Pleroma.User do end end + def image_description(image, default \\ "") + + def image_description(%{"name" => name}, _default), do: name + def image_description(_, default), do: default + # Should probably be renamed or removed @spec ap_id(User.t()) :: String.t() def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}" @@ -586,16 +591,26 @@ defmodule Pleroma.User do |> validate_length(:bio, max: bio_limit) |> validate_length(:name, min: 1, max: name_limit) |> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types()) + |> validate_image_description(:avatar_description, params) + |> validate_image_description(:header_description, params) |> put_fields() |> put_emoji() |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) - |> put_change_if_present(:avatar, &put_upload(&1, :avatar)) - |> put_change_if_present(:banner, &put_upload(&1, :banner)) + |> put_change_if_present( + :avatar, + &put_upload(&1, :avatar, Map.get(params, :avatar_description)) + ) + |> put_change_if_present( + :banner, + &put_upload(&1, :banner, Map.get(params, :header_description)) + ) |> put_change_if_present(:background, &put_upload(&1, :background)) |> put_change_if_present( :pleroma_settings_store, &{:ok, Map.merge(struct.pleroma_settings_store, &1)} ) + |> maybe_update_image_description(:avatar, Map.get(params, :avatar_description)) + |> maybe_update_image_description(:banner, Map.get(params, :header_description)) |> validate_fields(false) end @@ -674,13 +689,41 @@ defmodule Pleroma.User do end end - defp put_upload(value, type) do + defp put_upload(value, type, description \\ nil) do with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: type) do + {:ok, object} <- ActivityPub.upload(value, type: type, description: description) do {:ok, object.data} end end + defp validate_image_description(changeset, key, params) do + description_limit = Config.get([:instance, :description_limit], 5_000) + description = Map.get(params, key) + + if is_binary(description) and String.length(description) > description_limit do + changeset + |> add_error(key, "#{key} is too long") + else + changeset + end + end + + defp maybe_update_image_description(changeset, image_field, description) + when is_binary(description) do + with {:image_missing, true} <- {:image_missing, not changed?(changeset, image_field)}, + {:existing_image, %{"id" => id}} <- + {:existing_image, Map.get(changeset.data, image_field)}, + {:object, %Object{} = object} <- {:object, Object.get_by_ap_id(id)}, + {:ok, object} <- Object.update_data(object, %{"name" => description}) do + put_change(changeset, image_field, object.data) + else + {:description_too_long, true} -> {:error} + _ -> changeset + end + end + + defp maybe_update_image_description(changeset, _, _), do: changeset + def update_as_admin_changeset(struct, params) do struct |> update_changeset(params) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 7feaa22bf..cdff297a9 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -92,9 +92,6 @@ defmodule Pleroma.User.Backup do else true -> {:error, "Backup is missing id. Please insert it into the Repo first."} - - e -> - {:error, e} end end @@ -121,14 +118,13 @@ defmodule Pleroma.User.Backup do end defp permitted?(user) do - with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)}, - days = Config.get([__MODULE__, :limit_days]), - diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days), - {_, true} <- {:diff, diff > days} do - true + with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)} do + days = Config.get([__MODULE__, :limit_days]) + diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days) + + diff > days else {:last, nil} -> true - {:diff, false} -> false end end @@ -250,7 +246,13 @@ defmodule Pleroma.User.Backup do defp actor(dir, user) do with {:ok, json} <- UserView.render("user.json", %{user: user}) - |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) + |> Map.merge(%{ + "bookmarks" => "bookmarks.json", + "likes" => "likes.json", + "outbox" => "outbox.json", + "followers" => "followers.json", + "following" => "following.json" + }) |> Jason.encode() do File.write(Path.join(dir, "actor.json"), json) end @@ -297,9 +299,6 @@ defmodule Pleroma.User.Backup do ) acc - - _ -> - acc end end) diff --git a/lib/pleroma/user/import.ex b/lib/pleroma/user/import.ex index 11905237c..ab6bdb8d4 100644 --- a/lib/pleroma/user/import.ex +++ b/lib/pleroma/user/import.ex @@ -5,87 +5,107 @@ defmodule Pleroma.User.Import do use Ecto.Schema + alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Workers.BackgroundWorker require Logger - @spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()} - def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do - Enum.map( - identifiers, - fn identifier -> - with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier), - {:ok, _} <- User.mute(user, muted_user) do - muted_user - else - error -> handle_error(:mutes_import, identifier, error) - end - end - ) + @spec perform(atom(), User.t(), String.t()) :: :ok | {:error, any()} + def perform(:mute_import, %User{} = user, actor) do + with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor), + {_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)}, + {:ok, _} <- User.mute(user, muted_user) do + {:ok, muted_user} + else + {:existing_mute, true} -> :ok + error -> handle_error(:mutes_import, actor, error) + end end - def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do - Enum.map( - identifiers, - fn identifier -> - with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier), - {:ok, _block} <- CommonAPI.block(blocked, blocker) do - blocked - else - error -> handle_error(:blocks_import, identifier, error) - end - end - ) + def perform(:block_import, %User{} = user, actor) do + with {:ok, %User{} = blocked} <- User.get_or_fetch(actor), + {_, false} <- {:existing_block, User.blocks_user?(user, blocked)}, + {:ok, _block} <- CommonAPI.block(blocked, user) do + {:ok, blocked} + else + {:existing_block, true} -> :ok + error -> handle_error(:blocks_import, actor, error) + end end - def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do - Enum.map( - identifiers, - fn identifier -> - with {:ok, %User{} = followed} <- User.get_or_fetch(identifier), - {:ok, follower, followed} <- User.maybe_direct_follow(follower, followed), - {:ok, _, _, _} <- CommonAPI.follow(followed, follower) do - followed - else - error -> handle_error(:follow_import, identifier, error) - end - end - ) + def perform(:follow_import, %User{} = user, actor) do + with {:ok, %User{} = followed} <- User.get_or_fetch(actor), + {_, false} <- {:existing_follow, User.following?(user, followed)}, + {:ok, user, followed} <- User.maybe_direct_follow(user, followed), + {:ok, _, _, _} <- CommonAPI.follow(followed, user) do + {:ok, followed} + else + {:existing_follow, true} -> :ok + error -> handle_error(:follow_import, actor, error) + end end - def perform(_, _, _), do: :ok - defp handle_error(op, user_id, error) do Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}") - error + {:error, error} end - def blocks_import(%User{} = blocker, [_ | _] = identifiers) do - BackgroundWorker.new(%{ - "op" => "blocks_import", - "user_id" => blocker.id, - "identifiers" => identifiers - }) - |> Oban.insert() + def blocks_import(%User{} = user, [_ | _] = actors) do + jobs = + Repo.checkout(fn -> + Enum.reduce(actors, [], fn actor, acc -> + {:ok, job} = + BackgroundWorker.new(%{ + "op" => "block_import", + "user_id" => user.id, + "actor" => actor + }) + |> Oban.insert() + + acc ++ [job] + end) + end) + + {:ok, jobs} end - def follow_import(%User{} = follower, [_ | _] = identifiers) do - BackgroundWorker.new(%{ - "op" => "follow_import", - "user_id" => follower.id, - "identifiers" => identifiers - }) - |> Oban.insert() + def follows_import(%User{} = user, [_ | _] = actors) do + jobs = + Repo.checkout(fn -> + Enum.reduce(actors, [], fn actor, acc -> + {:ok, job} = + BackgroundWorker.new(%{ + "op" => "follow_import", + "user_id" => user.id, + "actor" => actor + }) + |> Oban.insert() + + acc ++ [job] + end) + end) + + {:ok, jobs} end - def mutes_import(%User{} = user, [_ | _] = identifiers) do - BackgroundWorker.new(%{ - "op" => "mutes_import", - "user_id" => user.id, - "identifiers" => identifiers - }) - |> Oban.insert() + def mutes_import(%User{} = user, [_ | _] = actors) do + jobs = + Repo.checkout(fn -> + Enum.reduce(actors, [], fn actor, acc -> + {:ok, job} = + BackgroundWorker.new(%{ + "op" => "mute_import", + "user_id" => user.id, + "actor" => actor + }) + |> Oban.insert() + + acc ++ [job] + end) + end) + + {:ok, jobs} end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a2a94a0ff..df8795fe4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1542,16 +1542,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp get_actor_url(_url), do: nil - defp normalize_image(%{"url" => url}) do + defp normalize_image(%{"url" => url} = data) do %{ "type" => "Image", "url" => [%{"href" => url}] } + |> maybe_put_description(data) end defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image() defp normalize_image(_), do: nil + defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do + Map.put(map, "name", description) + end + + defp maybe_put_description(map, _), do: map + defp object_to_user_data(data, additional) do fields = data diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index cdd054e1a..7ac0bbab4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -311,7 +311,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do post_inbox_relayed_create(conn, params) else conn - |> put_status(:bad_request) + |> put_status(403) |> json("Not federating") end end @@ -482,7 +482,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_status(:forbidden) |> json(message) - {:error, message} -> + {:error, message} when is_binary(message) -> conn |> put_status(:bad_request) |> json(message) diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index bc418d908..51ab476b7 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -108,6 +108,14 @@ defmodule Pleroma.Web.ActivityPub.MRF do def filter(%{} = object), do: get_policies() |> filter(object) + def id_filter(policies, id) when is_binary(id) do + policies + |> Enum.filter(&function_exported?(&1, :id_filter, 1)) + |> Enum.all?(& &1.id_filter(id)) + end + + def id_filter(id) when is_binary(id), do: get_policies() |> id_filter(id) + @impl true def pipeline_filter(%{} = message, meta) do object = meta[:object_data] diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index e4fcc9935..cf07db7f3 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -14,5 +14,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do end @impl true + def id_filter(id) do + Logger.debug("REJECTING #{id}") + false + end + + @impl true def describe, do: {:ok, %{}} end diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex index 54ca4b735..08bcac08a 100644 --- a/lib/pleroma/web/activity_pub/mrf/policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/policy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do @callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()} + @callback id_filter(String.t()) :: boolean() @callback describe() :: {:ok | :error, map()} @callback config_description() :: %{ optional(:children) => [map()], @@ -13,5 +14,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do description: String.t() } @callback history_awareness() :: :auto | :manual - @optional_callbacks config_description: 0, history_awareness: 0 + @optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1 end diff --git a/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex new file mode 100644 index 000000000..fa0610bf1 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/remote_report_policy.ex @@ -0,0 +1,118 @@ +defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do + @moduledoc "Drop remote reports if they don't contain enough information." + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.Config + + @impl true + def filter(%{"type" => "Flag"} = object) do + with {_, false} <- {:local, local?(object)}, + {:ok, _} <- maybe_reject_all(object), + {:ok, _} <- maybe_reject_anonymous(object), + {:ok, _} <- maybe_reject_third_party(object), + {:ok, _} <- maybe_reject_empty_message(object) do + {:ok, object} + else + {:local, true} -> {:ok, object} + {:reject, message} -> {:reject, message} + error -> {:reject, error} + end + end + + def filter(object), do: {:ok, object} + + defp maybe_reject_all(object) do + if Config.get([:mrf_remote_report, :reject_all]) do + {:reject, "[RemoteReportPolicy] Remote report"} + else + {:ok, object} + end + end + + defp maybe_reject_anonymous(%{"actor" => actor} = object) do + with true <- Config.get([:mrf_remote_report, :reject_anonymous]), + %URI{path: "/actor"} <- URI.parse(actor) do + {:reject, "[RemoteReportPolicy] Anonymous: #{actor}"} + else + _ -> {:ok, object} + end + end + + defp maybe_reject_third_party(%{"object" => objects} = object) do + {_, to} = + case objects do + [head | tail] when is_binary(head) -> {tail, head} + s when is_binary(s) -> {[], s} + _ -> {[], ""} + end + + with true <- Config.get([:mrf_remote_report, :reject_third_party]), + false <- String.starts_with?(to, Pleroma.Web.Endpoint.url()) do + {:reject, "[RemoteReportPolicy] Third-party: #{to}"} + else + _ -> {:ok, object} + end + end + + defp maybe_reject_empty_message(%{"content" => content} = object) + when is_binary(content) and content != "" do + {:ok, object} + end + + defp maybe_reject_empty_message(object) do + if Config.get([:mrf_remote_report, :reject_empty_message]) do + {:reject, ["RemoteReportPolicy] No content"]} + else + {:ok, object} + end + end + + defp local?(%{"actor" => actor}) do + String.starts_with?(actor, Pleroma.Web.Endpoint.url()) + end + + @impl true + def describe do + mrf_remote_report = + Config.get(:mrf_remote_report) + |> Enum.into(%{}) + + {:ok, %{mrf_remote_report: mrf_remote_report}} + end + + @impl true + def config_description do + %{ + key: :mrf_remote_report, + related_policy: "Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy", + label: "MRF Remote Report", + description: "Drop remote reports if they don't contain enough information.", + children: [ + %{ + key: :reject_all, + type: :boolean, + description: "Reject all remote reports? (this option takes precedence)", + suggestions: [false] + }, + %{ + key: :reject_anonymous, + type: :boolean, + description: "Reject anonymous remote reports?", + suggestions: [true] + }, + %{ + key: :reject_third_party, + type: :boolean, + description: "Reject reports on users from third-party instances?", + suggestions: [true] + }, + %{ + key: :reject_empty_message, + type: :boolean, + description: "Reject remote reports with no message?", + suggestions: [true] + } + ] + } + end +end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index ae7f18bfe..a97e8db7b 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -192,6 +192,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do end @impl true + def id_filter(id) do + host_info = URI.parse(id) + + with {:ok, _} <- check_accept(host_info, %{}), + {:ok, _} <- check_reject(host_info, %{}) do + true + else + _ -> false + end + end + + @impl true def filter(%{"type" => "Delete", "actor" => actor} = activity) do %{host: actor_host} = URI.parse(actor) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index abec9b038..ee12f3ebf 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating + import Pleroma.Constants, only: [activity_types: 0, object_types: 0] + alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object @@ -39,6 +41,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @impl true def validate(object, meta) + # This overload works together with the InboxGuardPlug + # and ensures that we are not accepting any activity type + # that cannot pass InboxGuardPlug. + # If we want to support any more activity types, make sure to + # add it in Pleroma.Constants's activity_types or object_types, + # and, if applicable, allowed_activity_types_from_strangers. + def validate(%{"type" => type}, _meta) + when type not in activity_types() and type not in object_types(), + do: {:error, :not_allowed_object_type} + def validate(%{"type" => "Block"} = block_activity, meta) do with {:ok, block_activity} <- block_activity @@ -165,7 +177,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do meta = Keyword.put(meta, :object_data, object_data), {:ok, update_activity} <- update_activity - |> UpdateValidator.cast_and_validate() + |> UpdateValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do update_activity = stringify_keys(update_activity) {:ok, update_activity, meta} @@ -173,7 +185,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do {:local, _} -> with {:ok, object} <- update_activity - |> UpdateValidator.cast_and_validate() + |> UpdateValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} @@ -203,9 +215,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do "Answer" -> AnswerValidator end + cast_func = + if type == "Update" do + fn o -> validator.cast_and_validate(o, meta) end + else + fn o -> validator.cast_and_validate(o) end + end + with {:ok, object} <- object - |> validator.cast_and_validate() + |> cast_func.() |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex index 4e27284aa..81ab354fe 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -85,6 +85,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do |> fix_replies() |> fix_attachments() |> CommonFixes.fix_quote_url() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> Transmogrifier.fix_content_map() |> CommonFixes.maybe_add_language() diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex index 65ac6bb93..034c6f33f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_image_video_validator.ex @@ -100,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() |> CommonFixes.fix_quote_url() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> fix_url() |> fix_content() diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index a9dc4a312..87d3e0c8f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -119,6 +119,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do def fix_quote_url(data), do: data + # On Mastodon, `"likes"` attribute includes an inlined `Collection` with `totalItems`, + # not a list of users. + # https://github.com/mastodon/mastodon/pull/32007 + def fix_likes(%{"likes" => %{}} = data), do: Map.drop(data, ["likes"]) + + def fix_likes(data), do: data + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md def object_link_tag?(%{ "type" => "Link", diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex index ec23770ad..ea14d6aca 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -48,6 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do data |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> CommonFixes.maybe_add_language() |> CommonFixes.maybe_add_content_map() diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 7f9d4d648..21940f4f1 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -64,6 +64,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do |> CommonFixes.fix_actor() |> CommonFixes.fix_object_defaults() |> CommonFixes.fix_quote_url() + |> CommonFixes.fix_likes() |> Transmogrifier.fix_emoji() |> fix_closed() end diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index 1e940a400..aab90235f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Object + alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -31,23 +33,50 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do |> cast(data, __schema__(:fields)) end - defp validate_data(cng) do + defp validate_data(cng, meta) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Update"]) |> validate_actor_presence() - |> validate_updating_rights() + |> validate_updating_rights(meta) end - def cast_and_validate(data) do + def cast_and_validate(data, meta \\ []) do data |> cast_data - |> validate_data + |> validate_data(meta) end - # For now we only support updating users, and here the rule is easy: - # object id == actor id - def validate_updating_rights(cng) do + def validate_updating_rights(cng, meta) do + if meta[:local] do + validate_updating_rights_local(cng) + else + validate_updating_rights_remote(cng) + end + end + + # For local Updates, verify the actor can edit the object + def validate_updating_rights_local(cng) do + actor = get_field(cng, :actor) + updated_object = get_field(cng, :object) + + if {:ok, actor} == ObjectValidators.ObjectID.cast(updated_object) do + cng + else + with %User{} = user <- User.get_cached_by_ap_id(actor), + {_, %Object{} = orig_object} <- {:object, Object.normalize(updated_object)}, + :ok <- Object.authorize_access(orig_object, user) do + cng + else + _e -> + cng + |> add_error(:object, "Can't be updated by this actor") + end + end + end + + # For remote Updates, verify the host is the same. + def validate_updating_rights_remote(cng) do with actor = get_field(cng, :actor), object = get_field(cng, :object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object), diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 7f11a4d67..fc36935d5 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -22,22 +22,27 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub) defp config, do: Config.get([:pipeline, :config], Config) - @spec common_pipeline(map(), keyword()) :: - {:ok, Activity.t() | Object.t(), keyword()} | {:error | :reject, any()} + @type results :: {:ok, Activity.t() | Object.t(), keyword()} + @type errors :: {:error | :reject, any()} + + # The Repo.transaction will wrap the result in an {:ok, _} + # and only returns an {:error, _} if the error encountered was related + # to the SQL transaction + @spec common_pipeline(map(), keyword()) :: results() | errors() def common_pipeline(object, meta) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do {:ok, {:ok, activity, meta}} -> side_effects().handle_after_transaction(meta) {:ok, activity, meta} - {:ok, value} -> - value + {:ok, {:error, _} = error} -> + error + + {:ok, {:reject, _} = error} -> + error {:error, e} -> {:error, e} - - {:reject, e} -> - {:reject, e} end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 937e4fd67..61975387b 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -127,10 +127,25 @@ defmodule Pleroma.Web.ActivityPub.UserView do "capabilities" => capabilities, "alsoKnownAs" => user.also_known_as, "vcard:bday" => birthday, - "webfinger" => "acct:#{User.full_nickname(user)}" + "webfinger" => "acct:#{User.full_nickname(user)}", + "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at) } - |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) - |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) + |> Map.merge( + maybe_make_image( + &User.avatar_url/2, + User.image_description(user.avatar, nil), + "icon", + user + ) + ) + |> Map.merge( + maybe_make_image( + &User.banner_url/2, + User.image_description(user.banner, nil), + "image", + user + ) + ) |> Map.merge(Utils.make_json_ld_header()) end @@ -305,16 +320,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do end end - defp maybe_make_image(func, key, user) do + defp maybe_make_image(func, description, key, user) do if image = func.(user, no_default: true) do %{ - key => %{ - "type" => "Image", - "url" => image - } + key => + %{ + "type" => "Image", + "url" => image + } + |> maybe_put_description(description) } else %{} end end + + defp maybe_put_description(map, description) when is_binary(description) do + Map.put(map, "name", description) + end + + defp maybe_put_description(map, _description), do: map end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index d9614bc48..21a779dcb 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -813,6 +813,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do allOf: [BooleanLike], nullable: true, description: "User's birthday will be visible" + }, + avatar_description: %Schema{ + type: :string, + nullable: true, + description: "Avatar image description." + }, + header_description: %Schema{ + type: :string, + nullable: true, + description: "Header image description." } }, example: %{ diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex index e6df21246..588b42e06 100644 --- a/lib/pleroma/web/api_spec/operations/media_operation.ex +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -121,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do security: [%{"oAuth" => ["write:media"]}], requestBody: Helpers.request_body("Parameters", create_request()), responses: %{ - 202 => Operation.response("Media", "application/json", Attachment), + 200 => Operation.response("Media", "application/json", Attachment), 400 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError), 500 => Operation.response("Media", "application/json", ApiError) diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 2dc0f66df..94d1f6b82 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -158,6 +158,10 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do type: :object, properties: %{ id: %Schema{type: :string}, + group_key: %Schema{ + type: :string, + description: "Group key shared by similar notifications" + }, type: notification_type(), created_at: %Schema{type: :string, format: :"date-time"}, account: %Schema{ @@ -180,6 +184,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do }, example: %{ "id" => "34975861", + "group-key" => "ungrouped-34975861", "type" => "mention", "created_at" => "2019-11-23T07:49:02.064Z", "account" => Account.schema().example, diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 8aeb821a8..1f73ef60c 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -111,7 +111,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do format: :uri, nullable: true, description: "Favicon image of the user's instance" - } + }, + avatar_description: %Schema{type: :string}, + header_description: %Schema{type: :string} } }, source: %Schema{ @@ -152,6 +154,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do example: %{ "acct" => "foobar", "avatar" => "https://mypleroma.com/images/avi.png", + "avatar_description" => "", "avatar_static" => "https://mypleroma.com/images/avi.png", "bot" => false, "created_at" => "2020-03-24T13:05:58.000Z", @@ -162,6 +165,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "followers_count" => 0, "following_count" => 1, "header" => "https://mypleroma.com/images/banner.png", + "header_description" => "", "header_static" => "https://mypleroma.com/images/banner.png", "id" => "9tKi3esbG7OQgZ2920", "locked" => false, diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 6e537b5da..25548d75b 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -249,6 +249,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do nullable: true, description: "A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned" + }, + list_id: %Schema{ + type: :integer, + nullable: true, + description: + "The ID of the list the post is addressed to (if any, only returned to author)" } } }, diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 01bf1575c..95be892cd 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -10,4 +10,9 @@ defmodule Pleroma.Web.Auth.Authenticator do @callback handle_error(Plug.Conn.t(), any()) :: any() @callback auth_template() :: String.t() | nil @callback oauth_consumer_template() :: String.t() | nil + + @callback change_password(Pleroma.User.t(), String.t(), String.t(), String.t()) :: + {:ok, Pleroma.User.t()} | {:error, term()} + + @optional_callbacks change_password: 4 end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index ea5620cf6..ec6601fb9 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -3,18 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.LDAPAuthenticator do + alias Pleroma.LDAP alias Pleroma.User - require Logger - - import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] + import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1] @behaviour Pleroma.Web.Auth.Authenticator @base Pleroma.Web.Auth.PleromaAuthenticator - @connection_timeout 10_000 - @search_timeout 10_000 - defdelegate get_registration(conn), to: @base defdelegate create_from_registration(conn, registration), to: @base defdelegate handle_error(conn, error), to: @base @@ -24,7 +20,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do def get_user(%Plug.Conn{} = conn) do with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])}, {:ok, {name, password}} <- fetch_credentials(conn), - %User{} = user <- ldap_user(name, password) do + %User{} = user <- LDAP.bind_user(name, password) do {:ok, user} else {:ldap, _} -> @@ -35,106 +31,12 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do end end - defp ldap_user(name, password) do - ldap = Pleroma.Config.get(:ldap, []) - host = Keyword.get(ldap, :host, "localhost") - port = Keyword.get(ldap, :port, 389) - ssl = Keyword.get(ldap, :ssl, false) - sslopts = Keyword.get(ldap, :sslopts, []) - - options = - [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++ - if sslopts != [], do: [{:sslopts, sslopts}], else: [] - - case :eldap.open([to_charlist(host)], options) do - {:ok, connection} -> - try do - if Keyword.get(ldap, :tls, false) do - :application.ensure_all_started(:ssl) - - case :eldap.start_tls( - connection, - Keyword.get(ldap, :tlsopts, []), - @connection_timeout - ) do - :ok -> - :ok - - error -> - Logger.error("Could not start TLS: #{inspect(error)}") - end - end - - bind_user(connection, ldap, name, password) - after - :eldap.close(connection) - end - - {:error, error} -> - Logger.error("Could not open LDAP connection: #{inspect(error)}") - {:error, {:ldap_connection_error, error}} - end - end - - defp bind_user(connection, ldap, name, password) do - uid = Keyword.get(ldap, :uid, "cn") - base = Keyword.get(ldap, :base) - - case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do - :ok -> - case fetch_user(name) do - %User{} = user -> - user - - _ -> - register_user(connection, base, uid, name) - end - - error -> - Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}") - {:error, {:ldap_bind_error, error}} - end - end - - defp register_user(connection, base, uid, name) do - case :eldap.search(connection, [ - {:base, to_charlist(base)}, - {:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))}, - {:scope, :eldap.wholeSubtree()}, - {:timeout, @search_timeout} - ]) do - # The :eldap_search_result record structure changed in OTP 24.3 and added a controls field - # https://github.com/erlang/otp/pull/5538 - {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} -> - try_register(name, attributes) - - {:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} -> - try_register(name, attributes) - - error -> - Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}") - {:error, {:ldap_search_error, error}} + def change_password(user, password, new_password, new_password) do + case LDAP.change_password(user.nickname, password, new_password) do + :ok -> {:ok, user} + e -> e end end - defp try_register(name, attributes) do - params = %{ - name: name, - nickname: name, - password: nil - } - - params = - case List.keyfind(attributes, ~c"mail", 0) do - {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) - _ -> params - end - - changeset = User.register_changeset_ldap(%User{}, params) - - case User.register(changeset) do - {:ok, user} -> user - error -> error - end - end + def change_password(_, _, _, _), do: {:error, :password_confirmation} end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 09a58eb66..0da3f19fc 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.AuthenticationPlug import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] @@ -101,4 +102,23 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def auth_template, do: nil def oauth_consumer_template, do: nil + + @doc "Changes Pleroma.User password in the database" + def change_password(user, password, new_password, new_password) do + case CommonAPI.Utils.confirm_current_password(user, password) do + {:ok, user} -> + with {:ok, _user} <- + User.reset_password(user, %{ + password: new_password, + password_confirmation: new_password + }) do + {:ok, user} + end + + error -> + error + end + end + + def change_password(_, _, _, _), do: {:error, :password_confirmation} end diff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex index a077cfa41..97b901036 100644 --- a/lib/pleroma/web/auth/wrapper_authenticator.ex +++ b/lib/pleroma/web/auth/wrapper_authenticator.ex @@ -39,4 +39,8 @@ defmodule Pleroma.Web.Auth.WrapperAuthenticator do implementation().oauth_consumer_template() || Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html") end + + @impl true + def change_password(user, password, new_password, new_password_confirmation), + do: implementation().change_password(user, password, new_password, new_password_confirmation) end diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 921e414c3..412424dae 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger - @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} + @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() def block(blocked, blocker) do with {:ok, block_data, _} <- Builder.block(blocker, blocked), {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do @@ -35,7 +35,7 @@ defmodule Pleroma.Web.CommonAPI do end @spec post_chat_message(User.t(), User.t(), String.t(), list()) :: - {:ok, Activity.t()} | {:error, any()} + {:ok, Activity.t()} | Pipeline.errors() def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), :ok <- validate_chat_attachment_attribution(maybe_attachment, user), @@ -58,7 +58,7 @@ defmodule Pleroma.Web.CommonAPI do )} do {:ok, activity} else - {:common_pipeline, {:reject, _} = e} -> e + {:common_pipeline, e} -> e e -> e end end @@ -99,7 +99,8 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} + @spec unblock(User.t(), User.t()) :: + {:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking} def unblock(blocked, blocker) do with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, {:ok, unblock_data, _} <- Builder.undo(blocker, block), @@ -120,7 +121,9 @@ defmodule Pleroma.Web.CommonAPI do end @spec follow(User.t(), User.t()) :: - {:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected} + {:ok, User.t(), User.t(), Activity.t() | Object.t()} + | {:error, :rejected} + | Pipeline.errors() def follow(followed, follower) do timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) @@ -145,7 +148,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} + @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() def accept_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, accept_data, _} <- Builder.accept(followed, follow_activity), @@ -154,7 +157,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil + @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil def reject_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, reject_data, _} <- Builder.reject(followed, follow_activity), @@ -163,7 +166,8 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} + @spec delete(String.t(), User.t()) :: + {:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()} def delete(activity_id, user) do with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- {:find_activity, Activity.get_by_id(activity_id, filter: [])}, @@ -213,7 +217,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} + @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found} def repeat(id, user, params \\ %{}) do with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), object = %Object{} <- Object.normalize(activity, fetch: false), @@ -231,7 +235,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} + @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()} def unrepeat(id, user) do with {_, %Activity{data: %{"type" => "Create"}} = activity} <- {:find_activity, Activity.get_by_id(id)}, @@ -247,7 +251,8 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} + @spec favorite(String.t(), User.t()) :: + {:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()} def favorite(id, %User{} = user) do case favorite_helper(user, id) do {:ok, _} = res -> @@ -285,7 +290,8 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} + @spec unfavorite(String.t(), User.t()) :: + {:ok, Activity.t()} | {:error, :not_found | String.t()} def unfavorite(id, user) do with {_, %Activity{data: %{"type" => "Create"}} = activity} <- {:find_activity, Activity.get_by_id(id)}, @@ -302,7 +308,7 @@ defmodule Pleroma.Web.CommonAPI do end @spec react_with_emoji(String.t(), User.t(), String.t()) :: - {:ok, Activity.t()} | {:error, any()} + {:ok, Activity.t()} | {:error, String.t()} def react_with_emoji(id, user, emoji) do with %Activity{} = activity <- Activity.get_by_id(id), object <- Object.normalize(activity, fetch: false), @@ -316,7 +322,7 @@ defmodule Pleroma.Web.CommonAPI do end @spec unreact_with_emoji(String.t(), User.t(), String.t()) :: - {:ok, Activity.t()} | {:error, any()} + {:ok, Activity.t()} | {:error, String.t()} def unreact_with_emoji(id, user, emoji) do with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji), {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)}, @@ -329,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()} + @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors() def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do with :ok <- validate_not_author(object, user), :ok <- validate_existing_votes(user, object), @@ -461,7 +467,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} + @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil} def update(orig_activity, %User{} = user, changes) do with orig_object <- Object.normalize(orig_activity), {:ok, new_object} <- make_update_data(user, orig_object, changes), @@ -497,7 +503,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} + @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() def pin(id, %User{} = user) do with %Activity{} = activity <- create_activity_by_id(id), true <- activity_belongs_to_actor(activity, user.ap_id), @@ -537,7 +543,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} + @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() def unpin(id, user) do with %Activity{} = activity <- create_activity_by_id(id), {:ok, unpin_data, _} <- Builder.unpin(user, activity.object), @@ -552,7 +558,7 @@ defmodule Pleroma.Web.CommonAPI do end end - @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} + @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()} def add_mute(activity, user, params \\ %{}) do expires_in = Map.get(params, :expires_in, 0) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index fef907ace..bab3c9fd0 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.Endpoint do websocket: [ path: "/", compress: false, + connect_info: [:sec_websocket_protocol], error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []}, fullsweep_after: 20 ] diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 4a0885fab..6637848a9 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do redirector_with_meta(conn, %{user: user}) else nil -> - redirector(conn, params) + redirector_with_meta(conn, Map.delete(params, "maybe_nickname_or_id")) end end diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 2df716556..58260afa8 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -102,7 +102,8 @@ defmodule Pleroma.Web.Federator do # NOTE: we use the actor ID to do the containment, this is fine because an # actor shouldn't be acting on objects outside their own AP server. - with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, + with {_, {:ok, user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, + {:user_active, true} <- {:user_active, match?(true, user.is_active)}, nil <- Activity.normalize(params["id"]), {_, :ok} <- {:correct_origin?, Containment.contain_origin_from_id(actor, params)}, @@ -121,11 +122,6 @@ defmodule Pleroma.Web.Federator do Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") {:error, e} - {:error, {:validate_object, _}} = e -> - Logger.error("Incoming AP doc validation error: #{inspect(e)}") - Logger.debug(Jason.encode!(params, pretty: true)) - e - e -> # Just drop those for now Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index e60767327..02d639296 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.Feed.FeedView def feed(conn, params) do - if Config.get!([:instance, :public]) do + if not Config.restrict_unauthenticated_access?(:timelines, :local) do render_feed(conn, params) else render_error(conn, :not_found, "Not found") @@ -18,10 +18,12 @@ defmodule Pleroma.Web.Feed.TagController do end defp render_feed(conn, %{"tag" => raw_tag} = params) do + local_only = Config.restrict_unauthenticated_access?(:timelines, :federated) + {format, tag} = parse_tag(raw_tag) activities = - %{type: ["Create"], tag: tag} + %{type: ["Create"], tag: tag, local_only: local_only} |> Pleroma.Maps.put_if_present(:max_id, params["max_id"]) |> ActivityPub.fetch_public_activities() diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 6657c2b3e..304313068 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -15,11 +15,11 @@ defmodule Pleroma.Web.Feed.UserController do action_fallback(:errors) - def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do + def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname} = params) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) else - _ -> Pleroma.Web.Fallback.RedirectController.redirector(conn, nil) + _ -> Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, params) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 54d46c86b..68157b0c4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -232,6 +232,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do |> Maps.put_if_present(:is_discoverable, params[:discoverable]) |> Maps.put_if_present(:birthday, params[:birthday]) |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) + |> Maps.put_if_present(:avatar_description, params[:avatar_description]) + |> Maps.put_if_present(:header_description, params[:header_description]) # What happens here: # @@ -277,6 +279,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do {:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} -> render_error(conn, :request_entity_too_large, "Name is too long") + {:error, %Ecto.Changeset{errors: [{:avatar_description, {_, _}} | _]}} -> + render_error(conn, :request_entity_too_large, "Avatar description is too long") + + {:error, %Ecto.Changeset{errors: [{:header_description, {_, _}} | _]}} -> + render_error(conn, :request_entity_too_large, "Banner description is too long") + {:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} -> render_error(conn, :request_entity_too_large, "One or more field entries are too long") diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index 844673ae0..6cfeb712e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Pleroma.Web.Plugs.RateLimiter, [name: :oauth_app_creation] when action == :create) + plug(:skip_auth when action in [:create, :verify_credentials]) plug(Pleroma.Web.ApiSpec.CastAndValidate) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 056bad844..41056d389 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -53,9 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do ) do attachment_data = Map.put(object.data, "id", object.id) - conn - |> put_status(202) - |> render("attachment.json", %{attachment: attachment_data}) + render(conn, "attachment.json", %{attachment: attachment_data}) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex index a2af8148c..6526457df 100644 --- a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.Workers.PollWorker action_fallback(Pleroma.Web.MastodonAPI.FallbackController) @@ -27,12 +28,16 @@ defmodule Pleroma.Web.MastodonAPI.PollController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + @poll_refresh_interval 120 @doc "GET /api/v1/polls/:id" def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do - with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + with %Object{} = object <- Object.get_by_id(id), + %Activity{} = activity <- + Activity.get_create_by_object_ap_id_with_object(object.data["id"]), true <- Visibility.visible_for_user?(activity, user) do + maybe_refresh_poll(activity) + try_render(conn, "show.json", %{object: object, for: user}) else error when is_nil(error) or error == false -> @@ -70,4 +75,13 @@ defmodule Pleroma.Web.MastodonAPI.PollController do end end) end + + defp maybe_refresh_poll(%Activity{object: %Object{} = object} = activity) do + with false <- activity.local, + {:ok, end_time} <- NaiveDateTime.from_iso8601(object.data["closed"]), + {_, :lt} <- {:closed_compare, NaiveDateTime.compare(object.updated_at, end_time)} do + PollWorker.new(%{"op" => "refresh", "activity_id" => activity.id}) + |> Oban.insert(unique: [period: @poll_refresh_interval]) + end + 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 6976ca6e5..f6727d29d 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -92,14 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do User.get_follow_state(reading_user, target) end - followed_by = - if following_relationships do - case FollowingRelationship.find(following_relationships, target, reading_user) do - %{state: :follow_accept} -> true - _ -> false - end - else - User.following?(target, reading_user) + followed_by = FollowingRelationship.following?(target, reading_user) + following = FollowingRelationship.following?(reading_user, target) + + requested = + cond do + following -> false + true -> match?(:follow_pending, follow_state) end subscribing = @@ -114,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags %{ id: to_string(target.id), - following: follow_state == :follow_accept, + following: following, followed_by: followed_by, blocking: UserRelationship.exists?( @@ -150,7 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do ), subscribing: subscribing, notifying: subscribing, - requested: follow_state == :follow_pending, + requested: requested, domain_blocking: User.blocks_domain?(reading_user, target), showing_reblogs: not UserRelationship.exists?( @@ -220,8 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do avatar = User.avatar_url(user) |> MediaProxy.url() avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true) + avatar_description = User.image_description(user.avatar) header = User.banner_url(user) |> MediaProxy.url() header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true) + header_description = User.image_description(user.banner) following_count = if !user.hide_follows_count or !user.hide_follows or self, @@ -322,7 +323,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do skip_thread_containment: user.skip_thread_containment, background_image: image_url(user.background) |> MediaProxy.url(), accepts_chat_messages: user.accepts_chat_messages, - favicon: favicon + favicon: favicon, + avatar_description: avatar_description, + header_description: header_description } } |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 3f2478719..c277af98b 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -95,6 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do response = %{ id: to_string(notification.id), + group_key: "ungrouped-" <> to_string(notification.id), type: notification.type, created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), account: account, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index a739b5d1a..10966edd6 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -465,7 +465,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do parent_visible: visible_for_user?(reply_to, opts[:for]), pinned_at: pinned_at, quotes_count: object.data["quotesCount"] || 0, - bookmark_folder: bookmark_folder + bookmark_folder: bookmark_folder, + list_id: get_list_id(object, client_posted_this_activity) } } end @@ -803,19 +804,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do defp build_application(_), do: nil - # Workaround for Elixir issue #10771 - # Avoid applying URI.merge unless necessary - # TODO: revert to always attempting URI.merge(image_url_data, page_url_data) - # when Elixir 1.12 is the minimum supported version - @spec build_image_url(struct() | nil, struct()) :: String.t() | nil - defp build_image_url( - %URI{scheme: image_scheme, host: image_host} = image_url_data, - %URI{} = _page_url_data - ) - when not is_nil(image_scheme) and not is_nil(image_host) do - image_url_data |> to_string - end - + @spec build_image_url(URI.t(), URI.t()) :: String.t() defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do URI.merge(page_url_data, image_url_data) |> to_string end @@ -851,4 +840,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do nil end end + + defp get_list_id(object, client_posted_this_activity) do + with true <- client_posted_this_activity, + %{data: %{"listMessage" => list_ap_id}} when is_binary(list_ap_id) <- object, + %{id: list_id} <- Pleroma.List.get_by_ap_id(list_ap_id) do + list_id + else + _ -> nil + end + end end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 730295a4c..3ed1cdd6c 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do # This only prepares the connection and is not in the process yet @impl Phoenix.Socket.Transport def connect(%{params: params} = transport_info) do - with access_token <- Map.get(params, "access_token"), + with access_token <- find_access_token(transport_info), {:ok, user, oauth_token} <- authenticate_request(access_token), {:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do @@ -244,4 +244,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do def handle_error(conn, _reason) do Plug.Conn.send_resp(conn, 404, "Not Found") end + + defp find_access_token(%{ + connect_info: %{sec_websocket_protocol: [token]} + }), + do: token + + defp find_access_token(%{params: %{"access_token" => token}}), do: token + + defp find_access_token(_), do: nil end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index 0b446e0a6..a0aafc32e 100644 --- a/lib/pleroma/web/media_proxy/media_proxy_controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -71,11 +71,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do drop_static_param_and_redirect(conn) content_type == "image/gif" -> - redirect(conn, external: media_proxy_url) + conn + |> put_status(301) + |> redirect(external: media_proxy_url) min_content_length_for_preview() > 0 and content_length > 0 and content_length < min_content_length_for_preview() -> - redirect(conn, external: media_proxy_url) + conn + |> put_status(301) + |> redirect(external: media_proxy_url) true -> handle_preview(content_type, conn, media_proxy_url) diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index 59d018730..4ee7c41ec 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.Metadata do def build_tags(params) do providers = [ + Pleroma.Web.Metadata.Providers.ActivityPub, Pleroma.Web.Metadata.Providers.RelMe, Pleroma.Web.Metadata.Providers.RestrictIndexing | activated_providers() diff --git a/lib/pleroma/web/metadata/providers/activity_pub.ex b/lib/pleroma/web/metadata/providers/activity_pub.ex new file mode 100644 index 000000000..bd9f92332 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/activity_pub.ex @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.ActivityPub do + alias Pleroma.Web.Metadata.Providers.Provider + + @behaviour Provider + + @impl Provider + def build_tags(%{object: %{data: %{"id" => object_id}}}) do + [{:link, [rel: "alternate", type: "application/activity+json", href: object_id], []}] + end + + @impl Provider + def build_tags(%{user: user}) do + [{:link, [rel: "alternate", type: "application/activity+json", href: user.ap_id], []}] + end + + @impl Provider + def build_tags(_), do: [] +end diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex index e97d6a54f..5a0f2338e 100644 --- a/lib/pleroma/web/metadata/providers/feed.ex +++ b/lib/pleroma/web/metadata/providers/feed.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do @behaviour Provider @impl Provider - def build_tags(%{user: user}) do + def build_tags(%{user: %{local: true} = user}) do [ {:link, [ @@ -20,4 +20,7 @@ defmodule Pleroma.Web.Metadata.Providers.Feed do ], []} ] end + + @impl Provider + def build_tags(_), do: [] end diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex index 97d3865ed..fa5fbe553 100644 --- a/lib/pleroma/web/metadata/providers/open_graph.ex +++ b/lib/pleroma/web/metadata/providers/open_graph.ex @@ -67,6 +67,9 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do end end + @impl Provider + def build_tags(_), do: [] + defp build_attachments(%{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index eabd8cb00..39aa71f06 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -20,6 +20,9 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do end) end + @impl Provider + def build_tags(_), do: [] + defp append_fields_tag(bio, fields) do fields |> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end) diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index 426022c65..7f50877c3 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -44,6 +44,9 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end end + @impl Provider + def build_tags(_), do: [] + defp title_tag(user) do {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []} end diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex index d1bf6dd18..7661c2566 100644 --- a/lib/pleroma/web/o_auth/app.ex +++ b/lib/pleroma/web/o_auth/app.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.OAuth.App do import Ecto.Query alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.OAuth.Token @type t :: %__MODULE__{} @@ -155,4 +156,29 @@ defmodule Pleroma.Web.OAuth.App do Map.put(acc, key, error) end) end + + @spec maybe_update_owner(Token.t()) :: :ok + def maybe_update_owner(%Token{app_id: app_id, user_id: user_id}) when not is_nil(user_id) do + __MODULE__.update(app_id, %{user_id: user_id}) + + :ok + end + + def maybe_update_owner(_), do: :ok + + @spec remove_orphans(pos_integer()) :: :ok + def remove_orphans(limit \\ 100) do + fifteen_mins_ago = DateTime.add(DateTime.utc_now(), -900, :second) + + Repo.transaction(fn -> + from(a in __MODULE__, + where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago, + limit: ^limit + ) + |> Repo.all() + |> Enum.each(&Repo.delete(&1)) + end) + + :ok + end end diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 47b03215f..0b3de5481 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -318,6 +318,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do + App.maybe_update_owner(token) + conn |> AuthHelper.put_session_token(token.token) |> json(OAuthView.render("token.json", view_params)) diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex index 96466f192..d65c30dab 100644 --- a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex @@ -38,8 +38,8 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@"))) |> Enum.reject(&(&1 == "")) - User.Import.follow_import(follower, identifiers) - json(conn, "job started") + User.Import.follows_import(follower, identifiers) + json(conn, "jobs started") end def blocks( @@ -55,7 +55,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do defp do_block(%{assigns: %{user: blocker}} = conn, list) do User.Import.blocks_import(blocker, prepare_user_identifiers(list)) - json(conn, "job started") + json(conn, "jobs started") end def mutes( @@ -71,7 +71,7 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do defp do_mute(%{assigns: %{user: user}} = conn, list) do User.Import.mutes_import(user, prepare_user_identifiers(list)) - json(conn, "job started") + json(conn, "jobs started") end defp prepare_user_identifiers(list) do diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex index f912a1542..af7d7f45a 100644 --- a/lib/pleroma/web/plugs/authentication_plug.ex +++ b/lib/pleroma/web/plugs/authentication_plug.ex @@ -47,6 +47,11 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do Pleroma.Password.Pbkdf2.verify_pass(password, password_hash) end + def checkpw(password, "$argon2" <> _ = password_hash) do + # Handle argon2 passwords for Akkoma migration + Argon2.verify_pass(password, password_hash) + end + def checkpw(_password, _password_hash) do Logger.error("Password hash not recognized") false @@ -56,6 +61,10 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do do_update_password(user, password) end + def maybe_update_password(%User{password_hash: "$argon2" <> _} = user, password) do + do_update_password(user, password) + end + def maybe_update_password(user, _), do: {:ok, user} defp do_update_password(user, password) do diff --git a/lib/pleroma/web/plugs/inbox_guard_plug.ex b/lib/pleroma/web/plugs/inbox_guard_plug.ex new file mode 100644 index 000000000..0064cce76 --- /dev/null +++ b/lib/pleroma/web/plugs/inbox_guard_plug.ex @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.InboxGuardPlug do + import Plug.Conn + import Pleroma.Constants, only: [activity_types: 0, allowed_activity_types_from_strangers: 0] + + alias Pleroma.Config + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{valid_signature: true}} = conn, _opts) do + with {_, true} <- {:federating, Config.get!([:instance, :federating])} do + conn + |> filter_activity_types() + else + {:federating, false} -> + conn + |> json(403, "Not federating") + |> halt() + end + end + + def call(conn, _opts) do + with {_, true} <- {:federating, Config.get!([:instance, :federating])}, + conn = filter_activity_types(conn), + {:known, true} <- {:known, known_actor?(conn)} do + conn + else + {:federating, false} -> + conn + |> json(403, "Not federating") + |> halt() + + {:known, false} -> + conn + |> filter_from_strangers() + end + end + + # Early rejection of unrecognized types + defp filter_activity_types(%{body_params: %{"type" => type}} = conn) do + with true <- type in activity_types() do + conn + else + _ -> + conn + |> json(400, "Invalid activity type") + |> halt() + end + end + + # If signature failed but we know this actor we should + # accept it as we may only need to refetch their public key + # during processing + defp known_actor?(%{body_params: data}) do + case Pleroma.Object.Containment.get_actor(data) |> User.get_cached_by_ap_id() do + %User{} -> true + _ -> false + end + end + + # Only permit a subset of activity types from strangers + # or else it will add actors you've never interacted with + # to the database + defp filter_from_strangers(%{body_params: %{"type" => type}} = conn) do + with true <- type in allowed_activity_types_from_strangers() do + conn + else + _ -> + conn + |> json(400, "Invalid activity type for an unknown actor") + |> halt() + end + end + + defp json(conn, status, resp) do + json_resp = Jason.encode!(resp) + + conn + |> put_resp_content_type("application/json") + |> resp(status, json_resp) + |> halt() + end +end diff --git a/lib/pleroma/web/push.ex b/lib/pleroma/web/push.ex index 6d777142e..77f77f88e 100644 --- a/lib/pleroma/web/push.ex +++ b/lib/pleroma/web/push.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.Push do end def vapid_config do - Application.get_env(:web_push_encryption, :vapid_details, nil) + Application.get_env(:web_push_encryption, :vapid_details, []) end def enabled, do: match?([subject: _, public_key: _, private_key: _], vapid_config()) diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index e2889b351..d4be97957 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -11,16 +11,39 @@ defmodule Pleroma.Web.RichMedia.Helpers do @spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors() def rich_media_get(url) do - headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] + case Pleroma.HTTP.AdapterHelper.can_stream?() do + true -> stream(url) + false -> head_first(url) + end + |> handle_result(url) + end + + defp stream(url) do + with {_, {:ok, %Tesla.Env{status: 200, body: stream_body, headers: headers}}} <- + {:get, Pleroma.HTTP.get(url, req_headers(), http_options())}, + {_, :ok} <- {:content_type, check_content_type(headers)}, + {_, :ok} <- {:content_length, check_content_length(headers)}, + {:read_stream, {:ok, body}} <- {:read_stream, read_stream(stream_body)} do + {:ok, body} + end + end + defp head_first(url) do with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <- - {:head, Pleroma.HTTP.head(url, headers, http_options())}, + {:head, Pleroma.HTTP.head(url, req_headers(), http_options())}, {_, :ok} <- {:content_type, check_content_type(headers)}, {_, :ok} <- {:content_length, check_content_length(headers)}, {_, {:ok, %Tesla.Env{status: 200, body: body}}} <- - {:get, Pleroma.HTTP.get(url, headers, http_options())} do + {:get, Pleroma.HTTP.get(url, req_headers(), http_options())} do {:ok, body} - else + end + end + + defp handle_result(result, url) do + case result do + {:ok, body} -> + {:ok, body} + {:head, _} -> Logger.debug("Rich media error for #{url}: HTTP HEAD failed") {:error, :head} @@ -29,8 +52,12 @@ defmodule Pleroma.Web.RichMedia.Helpers do Logger.debug("Rich media error for #{url}: content-type is #{type}") {:error, :content_type} - {:content_length, {_, length}} -> - Logger.debug("Rich media error for #{url}: content-length is #{length}") + {:content_length, :error} -> + Logger.debug("Rich media error for #{url}: content-length exceeded") + {:error, :body_too_large} + + {:read_stream, :error} -> + Logger.debug("Rich media error for #{url}: content-length exceeded") {:error, :body_too_large} {:get, _} -> @@ -59,7 +86,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do {_, maybe_content_length} -> case Integer.parse(maybe_content_length) do {content_length, ""} when content_length <= max_body -> :ok - {_, ""} -> {:error, maybe_content_length} + {_, ""} -> :error _ -> :ok end @@ -68,13 +95,37 @@ defmodule Pleroma.Web.RichMedia.Helpers do end end - defp http_options do - timeout = Config.get!([:rich_media, :timeout]) + defp read_stream(stream) do + max_body = Keyword.get(http_options(), :max_body) + + try do + result = + Stream.transform(stream, 0, fn chunk, total_bytes -> + new_total = total_bytes + byte_size(chunk) + + if new_total > max_body do + raise("Exceeds max body limit of #{max_body}") + else + {[chunk], new_total} + end + end) + |> Enum.into(<<>>) + {:ok, result} + rescue + _ -> :error + end + end + + defp http_options do [ pool: :rich_media, max_body: Config.get([:rich_media, :max_body], 5_000_000), - tesla_middleware: [{Tesla.Middleware.Timeout, timeout: timeout}] + stream: true ] end + + defp req_headers do + [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6492e3861..0423ca9e2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do end pipeline :well_known do - plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"]) + plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"]) end pipeline :config do @@ -217,6 +217,10 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) end + pipeline :inbox_guard do + plug(Pleroma.Web.Plugs.InboxGuardPlug) + end + pipeline :static_fe do plug(Pleroma.Web.Plugs.StaticFEPlug) end @@ -920,7 +924,7 @@ defmodule Pleroma.Web.Router do end scope "/", Pleroma.Web.ActivityPub do - pipe_through(:activitypub) + pipe_through([:activitypub, :inbox_guard]) post("/inbox", ActivityPubController, :inbox) post("/users/:nickname/inbox", ActivityPubController, :inbox) end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 6805233df..aeafa195d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Healthcheck alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.WebFinger @@ -195,19 +196,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn, _ ) do - case CommonAPI.Utils.confirm_current_password(user, body_params.password) do - {:ok, user} -> - with {:ok, _user} <- - User.reset_password(user, %{ - password: body_params.new_password, - password_confirmation: body_params.new_password_confirmation - }) do - json(conn, %{status: "success"}) - else - {:error, changeset} -> - {_, {error, _}} = Enum.at(changeset.errors, 0) - json(conn, %{error: "New password #{error}."}) - end + with {:ok, %User{}} <- + Authenticator.change_password( + user, + body_params.password, + body_params.new_password, + body_params.new_password_confirmation + ) do + json(conn, %{status: "success"}) + else + {:error, %Ecto.Changeset{} = changeset} -> + {_, {error, _}} = Enum.at(changeset.errors, 0) + json(conn, %{error: "New password #{error}."}) + + {:error, :password_confirmation} -> + json(conn, %{error: "New password does not match confirmation."}) {:error, msg} -> json(conn, %{error: msg}) diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/twitter_api/views/token_view.ex index 2e492c13f..36776ce3b 100644 --- a/lib/pleroma/web/twitter_api/views/token_view.ex +++ b/lib/pleroma/web/twitter_api/views/token_view.ex @@ -15,7 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.TokenView do %{ id: token_entry.id, valid_until: token_entry.valid_until, - app_name: token_entry.app.client_name + app_name: token_entry.app.client_name, + scopes: token_entry.scopes } end end diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 60da2d5ca..4737c6ea2 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -19,10 +19,10 @@ defmodule Pleroma.Workers.BackgroundWorker do User.perform(:force_password_reset, user) end - def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}}) - when op in ["blocks_import", "follow_import", "mutes_import"] do + def perform(%Job{args: %{"op" => op, "user_id" => user_id, "actor" => actor}}) + when op in ["block_import", "follow_import", "mute_import"] do user = User.get_cached_by_id(user_id) - {:ok, User.Import.perform(String.to_existing_atom(op), user, identifiers)} + User.Import.perform(String.to_existing_atom(op), user, actor) end def perform(%Job{ diff --git a/lib/pleroma/workers/cron/app_cleanup_worker.ex b/lib/pleroma/workers/cron/app_cleanup_worker.ex new file mode 100644 index 000000000..ee71cd7b6 --- /dev/null +++ b/lib/pleroma/workers/cron/app_cleanup_worker.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.AppCleanupWorker do + @moduledoc """ + Cleans up registered apps that were never associated with a user. + """ + + use Oban.Worker, queue: "background" + + alias Pleroma.Web.OAuth.App + + @impl true + def perform(_job) do + App.remove_orphans() + end + + @impl true + def timeout(_job), do: :timer.seconds(30) +end diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex index d263aa1b9..a9afe9d63 100644 --- a/lib/pleroma/workers/poll_worker.ex +++ b/lib/pleroma/workers/poll_worker.ex @@ -11,27 +11,46 @@ defmodule Pleroma.Workers.PollWorker do alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Object.Fetcher + + @stream_out_impl Pleroma.Config.get( + [__MODULE__, :stream_out], + Pleroma.Web.ActivityPub.ActivityPub + ) @impl true def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do - with %Activity{} = activity <- find_poll_activity(activity_id), + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)}, {:ok, notifications} <- Notification.create_poll_notifications(activity) do + unless activity.local do + # Schedule a final refresh + __MODULE__.new(%{"op" => "refresh", "activity_id" => activity_id}) + |> Oban.insert() + end + Notification.stream(notifications) else - {:error, :poll_activity_not_found} = e -> {:cancel, e} + {:activity, nil} -> {:cancel, :poll_activity_not_found} e -> {:error, e} end end - @impl true - def timeout(_job), do: :timer.seconds(5) + def perform(%Job{args: %{"op" => "refresh", "activity_id" => activity_id}}) do + with {_, %Activity{object: object}} <- + {:activity, Activity.get_by_id_with_object(activity_id)}, + {_, {:ok, _object}} <- {:refetch, Fetcher.refetch_object(object)} do + stream_update(activity_id) - defp find_poll_activity(activity_id) do - with nil <- Activity.get_by_id(activity_id) do - {:error, :poll_activity_not_found} + :ok + else + {:activity, nil} -> {:cancel, :poll_activity_not_found} + {:refetch, _} = e -> {:cancel, e} end end + @impl true + def timeout(_job), do: :timer.seconds(5) + def schedule_poll_end(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do with %Object{data: %{"type" => "Question", "closed" => closed}} when is_binary(closed) <- Object.normalize(activity), @@ -49,4 +68,10 @@ defmodule Pleroma.Workers.PollWorker do end def schedule_poll_end(activity), do: {:error, activity} + + defp stream_update(activity_id) do + Activity.get_by_id(activity_id) + |> Activity.normalize() + |> @stream_out_impl.stream_out() + end end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index d4db97b63..11b672bef 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do alias Pleroma.User alias Pleroma.Web.Federator - use Oban.Worker, queue: :federator_incoming, max_attempts: 5 + use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] @impl true @@ -33,7 +33,7 @@ defmodule Pleroma.Workers.ReceiverWorker do query_string: query_string } - with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), + with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do @@ -56,17 +56,29 @@ defmodule Pleroma.Workers.ReceiverWorker do def timeout(_job), do: :timer.seconds(5) + defp process_errors({:error, {:error, _} = error}), do: process_errors(error) + defp process_errors(errors) do case errors do - {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} + # User fetch failures + {:error, :not_found} = reason -> {:cancel, reason} + {:error, :forbidden} = reason -> {:cancel, reason} + # Inactive user + {:error, {:user_active, false} = reason} -> {:cancel, reason} + # Validator will error and return a changeset error + # e.g., duplicate activities or if the object was deleted + {:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason} + # Duplicate detection during Normalization {:error, :already_present} -> {:cancel, :already_present} - {:error, {:validate_object, _} = reason} -> {:cancel, reason} - {:error, {:error, {:validate, {:error, _changeset} = reason}}} -> {:cancel, reason} + # MRFs will return a reject {:error, {:reject, _} = reason} -> {:cancel, reason} + # HTTP Sigs {:signature, false} -> {:cancel, :invalid_signature} - {:error, "Object has been deleted"} = reason -> {:cancel, reason} + # Origin / URL validation failed somewhere possibly due to spoofing + {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} + # Unclear if this can be reached {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} - {:error, :not_found} = reason -> {:cancel, reason} + # Catchall {:error, _} = e -> e e -> {:error, e} end diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index 9d3f1ec53..aa09362f5 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do alias Pleroma.Object.Fetcher - use Oban.Worker, queue: :background + use Oban.Worker, queue: :background, unique: [period: :infinity] @impl true def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do diff --git a/lib/pleroma/workers/rich_media_worker.ex b/lib/pleroma/workers/rich_media_worker.ex index d5ba7b63e..e351ecd6e 100644 --- a/lib/pleroma/workers/rich_media_worker.ex +++ b/lib/pleroma/workers/rich_media_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.RichMediaWorker do alias Pleroma.Web.RichMedia.Backfill alias Pleroma.Web.RichMedia.Card - use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300] + use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: :infinity] @impl true def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do diff --git a/lib/pleroma/workers/user_refresh_worker.ex b/lib/pleroma/workers/user_refresh_worker.ex index 222a4a8f7..ee276774b 100644 --- a/lib/pleroma/workers/user_refresh_worker.ex +++ b/lib/pleroma/workers/user_refresh_worker.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.UserRefreshWorker do - use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: 300] + use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: :infinity] alias Pleroma.User diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index f4232d02a..879b26cc3 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.WebPusherWorker do alias Pleroma.Repo alias Pleroma.Web.Push.Impl - use Oban.Worker, queue: :web_push + use Oban.Worker, queue: :web_push, unique: [period: :infinity] @impl true def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do |