diff options
Diffstat (limited to 'lib/pleroma/web')
76 files changed, 1493 insertions, 730 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 73c6e4cbf..20f72e676 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -188,6 +188,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end) end + def stream_out_participations(%Object{data: %{"context" => context}}, user) do + with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), + conversation = Repo.preload(conversation, :participations), + last_activity_id = + fetch_latest_activity_id_for_context(conversation.ap_id, %{ + "user" => user, + "blocking_user" => user + }) do + if last_activity_id do + stream_out_participations(conversation.participations) + end + end + end + + def stream_out_participations(_, _), do: :noop + def stream_out(activity) do public = "https://www.w3.org/ns/activitystreams#Public" @@ -388,6 +404,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do + with data <- %{ + "to" => [follower_address], + "type" => "Delete", + "actor" => ap_id, + "object" => %{"type" => "Person", "id" => ap_id} + }, + {:ok, activity} <- insert(data, true, true), + :ok <- maybe_federate(activity) do + {:ok, user} + end + end + def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do user = User.get_cached_by_ap_id(actor) to = (object.data["to"] || []) ++ (object.data["cc"] || []) @@ -400,7 +429,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "to" => to, "deleted_activity_id" => activity && activity.id }, - {:ok, activity} <- insert(data, local), + {:ok, activity} <- insert(data, local, false), + stream_out_participations(object, user), _ <- decrease_replies_count_if_reply(object), # Changing note count prior to enqueuing federation task in order to avoid # race conditions on updating user.info diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 0182bda46..cf5176201 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -31,9 +31,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do conn else conn - |> put_status(404) - |> json(%{error: "not found"}) - |> halt + |> render_error(:not_found, "not found") + |> halt() end end @@ -190,7 +189,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do Logger.info(inspect(conn.req_headers)) end - json(conn, "error") + json(conn, dgettext("errors", "error")) end def relay(conn, _params) do @@ -218,9 +217,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_resp_header("content-type", "application/activity+json") |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) else + err = + dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}", + nickname: nickname, + as_nickname: user.nickname + ) + conn |> put_status(:forbidden) - |> json("can't read inbox of #{nickname} as #{user.nickname}") + |> json(err) end end @@ -246,7 +251,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} else - _ -> {:error, "Can't delete object"} + _ -> {:error, dgettext("errors", "Can't delete object")} end end @@ -255,12 +260,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {:ok, activity, _object} <- ActivityPub.like(user, object) do {:ok, activity} else - _ -> {:error, "Can't like object"} + _ -> {:error, dgettext("errors", "Can't like object")} end end def handle_user_activity(_, _) do - {:error, "Unhandled activity type"} + {:error, dgettext("errors", "Unhandled activity type")} end def update_outbox( @@ -288,22 +293,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(message) end else + err = + dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", + nickname: nickname, + as_nickname: user.nickname + ) + conn |> put_status(:forbidden) - |> json("can't update outbox of #{nickname} as #{user.nickname}") + |> json(err) end end def errors(conn, {:error, :not_found}) do conn - |> put_status(404) - |> json("Not found") + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) end def errors(conn, _e) do conn - |> put_status(500) - |> json("error") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "error")) end defp set_requester_reachable(%Plug.Conn{} = conn, _) do diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex new file mode 100644 index 000000000..2da3eac2f --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do + alias Pleroma.User + + require Logger + + # has the user successfully posted before? + defp old_user?(%User{} = u) do + u.info.note_count > 0 || u.info.follower_count > 0 + end + + # does the post contain links? + defp contains_links?(%{"content" => content} = _object) do + content + |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl") + |> Floki.attribute("a", "href") + |> length() > 0 + end + + defp contains_links?(_), do: false + + def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do + with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), + {:contains_links, true} <- {:contains_links, contains_links?(object)}, + {:old_user, true} <- {:old_user, old_user?(u)} do + {:ok, message} + else + {:contains_links, false} -> + {:ok, message} + + {:old_user, false} -> + {:reject, nil} + + {:error, _} -> + {:reject, nil} + + e -> + Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") + {:reject, nil} + end + end + + # in all other cases, pass through + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 15d8514be..2d03df68a 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -9,8 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do @behaviour Pleroma.Web.ActivityPub.MRF @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) + def filter_by_summary( - %{"summary" => parent_summary} = _parent, + %{data: %{"summary" => parent_summary}} = _in_reply_to, %{"summary" => child_summary} = child ) when not is_nil(child_summary) and byte_size(child_summary) > 0 and @@ -24,17 +25,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do end end - def filter_by_summary(_parent, child), do: child - - def filter(%{"type" => activity_type} = object) when activity_type == "Create" do - child = object["object"] - in_reply_to = Object.normalize(child["inReplyTo"]) + def filter_by_summary(_in_reply_to, child), do: child + def filter(%{"type" => "Create", "object" => child_object} = object) do child = - if(in_reply_to, - do: filter_by_summary(in_reply_to.data, child), - else: child - ) + child_object["inReplyTo"] + |> Object.normalize(child_object["inReplyTo"]) + |> filter_by_summary(child_object) object = Map.put(object, "object", child) diff --git a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex new file mode 100644 index 000000000..01d21a299 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do + @moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them" + @behaviour Pleroma.Web.ActivityPub.MRF + + alias Pleroma.HTTP + alias Pleroma.Web.MediaProxy + + require Logger + + @hackney_options [ + pool: :media, + recv_timeout: 10_000 + ] + + def perform(:prefetch, url) do + Logger.info("Prefetching #{inspect(url)}") + + url + |> MediaProxy.url() + |> HTTP.get([], adapter: @hackney_options) + end + + def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do + Enum.each(attachments, fn + %{"url" => url} when is_list(url) -> + url + |> Enum.each(fn + %{"href" => href} -> + PleromaJobQueue.enqueue(:background, __MODULE__, [:prefetch, href]) + + x -> + Logger.debug("Unhandled attachment URL object #{inspect(x)}") + end) + + x -> + Logger.debug("Unhandled attachment #{inspect(x)}") + end) + end + + @impl true + def filter( + %{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message + ) + when is_list(attachments) and length(attachments) > 0 do + PleromaJobQueue.enqueue(:background, __MODULE__, [:preload, message]) + + {:ok, message} + end + + @impl true + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index f30fee0d5..86a48bda5 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -10,19 +10,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do def filter( %{ "type" => "Create", - "object" => %{"content" => content, "attachment" => _attachment} = child_object + "object" => %{"content" => content, "attachment" => _} = _child_object } = object ) when content in [".", "<p>.</p>"] do - child_object = - child_object - |> Map.put("content", "") - - object = - object - |> Map.put("object", child_object) - - {:ok, object} + {:ok, put_in(object, ["object", "content"], "")} end @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 9c87c6963..c269d0f89 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -8,18 +8,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do @behaviour Pleroma.Web.ActivityPub.MRF - def filter(%{"type" => activity_type} = object) when activity_type == "Create" do + def filter(%{"type" => "Create", "object" => child_object} = object) do scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy]) - child = object["object"] - content = - child["content"] + child_object["content"] |> HTML.filter_tags(scrub_policy) - child = Map.put(child, "content", content) - - object = Map.put(object, "object", child) + object = put_in(object, ["object", "content"], content) {:ok, object} end diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index ea3df1b4d..da13fd7c7 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -3,46 +3,42 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do - alias Pleroma.User @moduledoc "Rejects non-public (followers-only, direct) activities" + + alias Pleroma.Config + alias Pleroma.User + @behaviour Pleroma.Web.ActivityPub.MRF + @public "https://www.w3.org/ns/activitystreams#Public" + @impl true def filter(%{"type" => "Create"} = object) do user = User.get_cached_by_ap_id(object["actor"]) - public = "https://www.w3.org/ns/activitystreams#Public" # Determine visibility visibility = cond do - public in object["to"] -> "public" - public in object["cc"] -> "unlisted" + @public in object["to"] -> "public" + @public in object["cc"] -> "unlisted" user.follower_address in object["to"] -> "followers" true -> "direct" end - policy = Pleroma.Config.get(:mrf_rejectnonpublic) + policy = Config.get(:mrf_rejectnonpublic) + + cond do + visibility in ["public", "unlisted"] -> + {:ok, object} - case visibility do - "public" -> + visibility == "followers" and Keyword.get(policy, :allow_followersonly) -> {:ok, object} - "unlisted" -> + visibility == "direct" and Keyword.get(policy, :allow_direct) -> {:ok, object} - "followers" -> - with true <- Keyword.get(policy, :allow_followersonly) do - {:ok, object} - else - _e -> {:reject, nil} - end - - "direct" -> - with true <- Keyword.get(policy, :allow_direct) do - {:ok, object} - else - _e -> {:reject, nil} - end + true -> + {:reject, nil} end end diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index 6683b8d8e..b42c4ed76 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -19,12 +19,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do - `mrf_tag:disable-any-subscription`: Reject any follow requests """ + @public "https://www.w3.org/ns/activitystreams#Public" + defp get_tags(%User{tags: tags}) when is_list(tags), do: tags defp get_tags(_), do: [] defp process_tag( "mrf_tag:media-force-nsfw", - %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message + %{ + "type" => "Create", + "object" => %{"attachment" => child_attachment} = object + } = message ) when length(child_attachment) > 0 do tags = (object["tag"] || []) ++ ["nsfw"] @@ -41,7 +46,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defp process_tag( "mrf_tag:media-strip", - %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message + %{ + "type" => "Create", + "object" => %{"attachment" => child_attachment} = object + } = message ) when length(child_attachment) > 0 do object = Map.delete(object, "attachment") @@ -52,19 +60,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defp process_tag( "mrf_tag:force-unlisted", - %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "actor" => actor, + "object" => object + } = message ) do user = User.get_cached_by_ap_id(actor) - if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") do - to = - List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] - - cc = - List.delete(cc, user.follower_address) ++ ["https://www.w3.org/ns/activitystreams#Public"] + if Enum.member?(to, @public) do + to = List.delete(to, @public) ++ [user.follower_address] + cc = List.delete(cc, user.follower_address) ++ [@public] object = - message["object"] + object |> Map.put("to", to) |> Map.put("cc", cc) @@ -82,19 +93,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defp process_tag( "mrf_tag:sandbox", - %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "actor" => actor, + "object" => object + } = message ) do user = User.get_cached_by_ap_id(actor) - if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") or - Enum.member?(cc, "https://www.w3.org/ns/activitystreams#Public") do - to = - List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] - - cc = List.delete(cc, "https://www.w3.org/ns/activitystreams#Public") + if Enum.member?(to, @public) or Enum.member?(cc, @public) do + to = List.delete(to, @public) ++ [user.follower_address] + cc = List.delete(cc, @public) object = - message["object"] + object |> Map.put("to", to) |> Map.put("cc", cc) @@ -123,7 +137,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do end end - defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil} + defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), + do: {:reject, nil} defp process_tag(_, message), do: {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex index 47663414a..e35d2c422 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex @@ -21,7 +21,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do @impl true def filter(%{"actor" => actor} = object) do actor_info = URI.parse(actor) - allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], []) + + allow_list = + Config.get( + [:mrf_user_allowlist, String.to_atom(actor_info.host)], + [] + ) filter_by_list(object, allow_list) end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index f376e5618..b7dc90caa 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do true else inbox_info = URI.parse(inbox) - !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) + !Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host) end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d22d24479..ad741122f 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Federator import Ecto.Query @@ -22,20 +23,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do @doc """ Modifies an incoming AP object (mastodon format) to our internal format. """ - def fix_object(object) do + def fix_object(object, options \\ []) do object |> fix_actor |> fix_url |> fix_attachments |> fix_context - |> fix_in_reply_to + |> fix_in_reply_to(options) |> fix_emoji |> fix_tag |> fix_content_map |> fix_likes |> fix_addressing |> fix_summary - |> fix_type + |> fix_type(options) end def fix_summary(%{"summary" => nil} = object) do @@ -164,7 +165,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do object end - def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) + def fix_in_reply_to(object, options \\ []) + + def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) when not is_nil(in_reply_to) do in_reply_to_id = cond do @@ -182,28 +185,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "" end - case get_obj_helper(in_reply_to_id) do - {:ok, replied_object} -> - with %Activity{} = _activity <- - Activity.get_create_by_object_ap_id(replied_object.data["id"]) do - object - |> Map.put("inReplyTo", replied_object.data["id"]) - |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) - |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) - |> Map.put("context", replied_object.data["context"] || object["conversation"]) - else - e -> - Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") + object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) + + if Federator.allowed_incoming_reply_depth?(options[:depth]) do + case get_obj_helper(in_reply_to_id, options) do + {:ok, replied_object} -> + with %Activity{} = _activity <- + Activity.get_create_by_object_ap_id(replied_object.data["id"]) do object - end + |> Map.put("inReplyTo", replied_object.data["id"]) + |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) + |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) + |> Map.put("context", replied_object.data["context"] || object["conversation"]) + else + e -> + Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") + object + end - e -> - Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") - object + e -> + Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") + object + end + else + object end end - def fix_in_reply_to(object), do: object + def fix_in_reply_to(object, _options), do: object def fix_context(object) do context = object["context"] || object["conversation"] || Utils.generate_context_id() @@ -336,17 +345,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_content_map(object), do: object - def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do - reply = Object.normalize(reply_id) + def fix_type(object, options \\ []) - if reply.data["type"] == "Question" and object["name"] do + def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do + reply = + if Federator.allowed_incoming_reply_depth?(options[:depth]) do + Object.normalize(reply_id, true) + end + + if reply && (reply.data["type"] == "Question" and object["name"]) do Map.put(object, "type", "Answer") else object end end - def fix_type(object), do: object + def fix_type(object, _), do: object defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do with true <- id =~ "follows", @@ -374,9 +388,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end + def handle_incoming(data, options \\ []) + # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. - def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do + def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -409,15 +425,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end # disallow objects with bogus IDs - def handle_incoming(%{"id" => nil}), do: :error - def handle_incoming(%{"id" => ""}), do: :error + def handle_incoming(%{"id" => nil}, _options), do: :error + def handle_incoming(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error + def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8), + do: :error # TODO: validate those with a Ecto scheme # - tags # - emoji - def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, + options + ) when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do actor = Containment.get_actor(data) @@ -427,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do with nil <- Activity.get_create_by_object_ap_id(object["id"]), {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do - object = fix_object(data["object"]) + options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) + object = fix_object(data["object"], options) params = %{ to: data["to"], @@ -452,16 +473,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data + %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, + _options ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), - {:user_blocked, false} <- + {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, - {:user_locked, false} <- {:user_locked, User.locked?(followed)}, - {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do + {_, false} <- {:user_locked, User.locked?(followed)}, + {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, + {_, {:ok, _}} <- + {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do ActivityPub.accept(%{ to: [follower.ap_id], actor: followed, @@ -470,7 +494,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do }) else {:user_blocked, true} -> - {:ok, _} = Utils.update_follow_state(activity, "reject") + {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") ActivityPub.reject(%{ to: [follower.ap_id], @@ -480,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do }) {:follow, {:error, _}} -> - {:ok, _} = Utils.update_follow_state(activity, "reject") + {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") ActivityPub.reject(%{ to: [follower.ap_id], @@ -501,38 +525,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data + %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), {:ok, follow_activity} <- get_follow_activity(follow_object, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), - {:ok, activity} <- - ActivityPub.accept(%{ - to: follow_activity.data["to"], - type: "Accept", - actor: followed, - object: follow_activity.data["id"], - local: false - }) do - if not User.following?(follower, followed) do - {:ok, _follower} = User.follow(follower, followed) - end - - {:ok, activity} + {:ok, _follower} = User.follow(follower, followed) do + ActivityPub.accept(%{ + to: follow_activity.data["to"], + type: "Accept", + actor: followed, + object: follow_activity.data["id"], + local: false + }) else _e -> :error end end def handle_incoming( - %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data + %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), {:ok, follow_activity} <- get_follow_activity(follow_object, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), {:ok, activity} <- ActivityPub.reject(%{ @@ -551,7 +572,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data + %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -564,7 +586,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data + %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -579,7 +602,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} = - data + data, + _options ) when object_type in ["Person", "Application", "Service", "Organization"] do with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do @@ -617,7 +641,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # an error or a tombstone. This would allow us to verify that a deletion actually took # place. def handle_incoming( - %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data + %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data, + _options ) do object_id = Utils.get_ap_id(object_id) @@ -628,7 +653,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, activity} <- ActivityPub.delete(object, false) do {:ok, activity} else - _e -> :error + nil -> + case User.get_cached_by_ap_id(object_id) do + %User{ap_id: ^actor} = user -> + {:ok, followers} = User.get_followers(user) + + Enum.each(followers, fn follower -> + User.unfollow(follower, user) + end) + + {:ok, friends} = User.get_friends(user) + + Enum.each(friends, fn followed -> + User.unfollow(user, followed) + end) + + User.invalidate_cache(user) + Repo.delete(user) + + nil -> + :error + end + + _e -> + :error end end @@ -638,7 +686,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Announce", "object" => object_id}, "actor" => _actor, "id" => id - } = data + } = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -656,7 +705,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Follow", "object" => followed}, "actor" => follower, "id" => id - } = _data + } = _data, + _options ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), @@ -674,7 +724,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Block", "object" => blocked}, "actor" => blocker, "id" => id - } = _data + } = _data, + _options ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), @@ -688,7 +739,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data + %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data, + _options ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), @@ -708,7 +760,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Like", "object" => object_id}, "actor" => _actor, "id" => id - } = data + } = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -720,10 +773,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(_), do: :error + def handle_incoming(_, _), do: :error - def get_obj_helper(id) do - if object = Object.normalize(id), do: {:ok, object}, else: nil + def get_obj_helper(id, options \\ []) do + if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil end def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index faae7e747..4288ea4c8 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -151,16 +151,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do def create_context(context) do context = context || generate_id("contexts") - changeset = Object.context_mapping(context) - case Repo.insert(changeset) do - {:ok, object} -> - object + # Ecto has problems accessing the constraint inside the jsonb, + # so we explicitly check for the existed object before insert + object = Object.get_cached_by_ap_id(context) - # This should be solved by an upsert, but it seems ecto - # has problems accessing the constraint inside the jsonb. - {:error, _} -> - Object.get_cached_by_ap_id(context) + with true <- is_nil(object), + changeset <- Object.context_mapping(context), + {:ok, inserted_object} <- Repo.insert(changeset) do + inserted_object + else + _ -> + object end end @@ -168,14 +170,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do Enqueues an activity for federation if it's local """ def maybe_federate(%Activity{local: true} = activity) do - priority = - case activity.data["type"] do - "Delete" -> 10 - "Create" -> 1 - _ -> 5 - end + if Pleroma.Config.get!([:instance, :federating]) do + priority = + case activity.data["type"] do + "Delete" -> 10 + "Create" -> 1 + _ -> 5 + end + + Pleroma.Web.Federator.publish(activity, priority) + end - Pleroma.Web.Federator.publish(activity, priority) :ok end @@ -376,8 +381,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Updates a follow activity's state (for locked accounts). """ - def update_follow_state( - %Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity, + def update_follow_state_for_all( + %Activity{data: %{"actor" => actor, "object" => object}} = activity, state ) do try do diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 8965e3253..9908a2e75 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.Visibility do alias Pleroma.Activity alias Pleroma.Object diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de2a13c01..8b3c3c91f 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.AdminAPI.Config + alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI @@ -72,7 +74,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def user_show(conn, %{"nickname" => nickname}) do - with %User{} = user <- User.get_cached_by_nickname(nickname) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do conn |> json(AccountView.render("show.json", %{user: user})) else @@ -158,9 +160,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_add(conn, _) do - conn - |> put_status(404) - |> json(%{error: "No such permission_group"}) + render_error(conn, :not_found, "No such permission_group") end def right_get(conn, %{"nickname" => nickname}) do @@ -182,9 +182,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do ) when permission_group in ["moderator", "admin"] do if admin_nickname == nickname do - conn - |> put_status(403) - |> json(%{error: "You can't revoke your own admin status."}) + render_error(conn, :forbidden, "You can't revoke your own admin status.") else user = User.get_cached_by_nickname(nickname) @@ -205,9 +203,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_delete(conn, _) do - conn - |> put_status(404) - |> json(%{error: "No such permission_group"}) + render_error(conn, :not_found, "No such permission_group") end def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do @@ -362,28 +358,63 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + def config_show(conn, _params) do + configs = Pleroma.Repo.all(Config) + + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: configs}) + end + + def config_update(conn, %{"configs" => configs}) do + updated = + if Pleroma.Config.get([:instance, :dynamic_configuration]) do + updated = + Enum.map(configs, fn + %{"group" => group, "key" => key, "value" => value} -> + {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) + config + + %{"group" => group, "key" => key, "delete" => "true"} -> + {:ok, _} = Config.delete(%{group: group, key: key}) + nil + end) + |> Enum.reject(&is_nil(&1)) + + Pleroma.Config.TransferTask.load_and_update_env() + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"]) + updated + else + [] + end + + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: updated}) + end + def errors(conn, {:error, :not_found}) do conn - |> put_status(404) - |> json("Not found") + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) end def errors(conn, {:error, reason}) do conn - |> put_status(400) + |> put_status(:bad_request) |> json(reason) end def errors(conn, {:param_cast, _}) do conn - |> put_status(400) - |> json("Invalid parameters") + |> put_status(:bad_request) + |> json(dgettext("errors", "Invalid parameters")) end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end defp page_params(params) do diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex new file mode 100644 index 000000000..24674abc5 --- /dev/null +++ b/lib/pleroma/web/admin_api/config.ex @@ -0,0 +1,165 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.Config do + use Ecto.Schema + import Ecto.Changeset + import Pleroma.Web.Gettext + alias __MODULE__ + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + schema "config" do + field(:key, :string) + field(:group, :string) + field(:value, :binary) + + timestamps() + end + + @spec get_by_params(map()) :: Config.t() | nil + def get_by_params(params), do: Repo.get_by(Config, params) + + @spec changeset(Config.t(), map()) :: Changeset.t() + def changeset(config, params \\ %{}) do + config + |> cast(params, [:key, :group, :value]) + |> validate_required([:key, :group, :value]) + |> unique_constraint(:key, name: :config_group_key_index) + end + + @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def create(params) do + %Config{} + |> changeset(Map.put(params, :value, transform(params[:value]))) + |> Repo.insert() + end + + @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()} + def update(%Config{} = config, %{value: value}) do + config + |> change(value: transform(value)) + |> Repo.update() + end + + @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def update_or_create(params) do + with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do + Config.update(config, params) + else + nil -> Config.create(params) + end + end + + @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def delete(params) do + with %Config{} = config <- Config.get_by_params(params) do + Repo.delete(config) + else + nil -> + err = + dgettext("errors", "Config with params %{params} not found", params: inspect(params)) + + {:error, err} + end + end + + @spec from_binary(binary()) :: term() + def from_binary(value), do: :erlang.binary_to_term(value) + + @spec from_binary_to_map(binary()) :: any() + def from_binary_to_map(binary) do + from_binary(binary) + |> do_convert() + end + + defp do_convert([{k, v}] = value) when is_list(value) and length(value) == 1, + do: %{k => do_convert(v)} + + defp do_convert(values) when is_list(values), do: for(val <- values, do: do_convert(val)) + + defp do_convert({k, v} = value) when is_tuple(value), + do: %{k => do_convert(v)} + + defp do_convert(value) when is_tuple(value), do: %{"tuple" => do_convert(Tuple.to_list(value))} + + defp do_convert(value) when is_binary(value) or is_map(value) or is_number(value), do: value + + defp do_convert(value) when is_atom(value) do + string = to_string(value) + + if String.starts_with?(string, "Elixir."), + do: String.trim_leading(string, "Elixir."), + else: value + end + + @spec transform(any()) :: binary() + def transform(%{"tuple" => _} = entity), do: :erlang.term_to_binary(do_transform(entity)) + + def transform(entity) when is_map(entity) do + tuples = + for {k, v} <- entity, + into: [], + do: {if(is_atom(k), do: k, else: String.to_atom(k)), do_transform(v)} + + Enum.reject(tuples, fn {_k, v} -> is_nil(v) end) + |> Enum.sort() + |> :erlang.term_to_binary() + end + + def transform(entity) when is_list(entity) do + list = Enum.map(entity, &do_transform(&1)) + :erlang.term_to_binary(list) + end + + def transform(entity), do: :erlang.term_to_binary(entity) + + defp do_transform(%Regex{} = value) when is_map(value), do: value + + defp do_transform(%{"tuple" => [k, values] = entity}) when length(entity) == 2 do + {do_transform(k), do_transform(values)} + end + + defp do_transform(%{"tuple" => values}) do + Enum.reduce(values, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) + end + + defp do_transform(value) when is_map(value) do + values = for {key, val} <- value, into: [], do: {String.to_atom(key), do_transform(val)} + + Enum.sort(values) + end + + defp do_transform(value) when is_list(value) do + Enum.map(value, &do_transform(&1)) + end + + defp do_transform(entity) when is_list(entity) and length(entity) == 1, do: hd(entity) + + defp do_transform(value) when is_binary(value) do + String.trim(value) + |> do_transform_string() + end + + defp do_transform(value), do: value + + defp do_transform_string(value) when byte_size(value) == 0, do: nil + + defp do_transform_string(value) do + cond do + String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix") -> + String.to_existing_atom("Elixir." <> value) + + String.starts_with?(value, ":") -> + String.replace(value, ":", "") |> String.to_existing_atom() + + String.starts_with?(value, "i:") -> + String.replace(value, "i:", "") |> String.to_integer() + + true -> + value + end + end +end diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 28bb667d8..7e1b9c431 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -5,8 +5,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do use Pleroma.Web, :view + alias Pleroma.HTML + alias Pleroma.User alias Pleroma.User.Info alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.MediaProxy def render("index.json", %{users: users, count: count, page_size: page_size}) do %{ @@ -17,9 +20,14 @@ defmodule Pleroma.Web.AdminAPI.AccountView do end def render("show.json", %{user: user}) do + avatar = User.avatar_url(user) |> MediaProxy.url() + display_name = HTML.strip_tags(user.name || user.nickname) + %{ "id" => user.id, + "avatar" => avatar, "nickname" => user.nickname, + "display_name" => display_name, "deactivated" => user.info.deactivated, "local" => user.local, "roles" => Info.roles(user.info), diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex new file mode 100644 index 000000000..a31f1041f --- /dev/null +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigView do + use Pleroma.Web, :view + + def render("index.json", %{configs: configs}) do + %{ + configs: render_many(configs, __MODULE__, "show.json", as: :config) + } + end + + def render("show.json", %{config: config}) do + %{ + key: config.key, + group: config.group, + value: Pleroma.Web.AdminAPI.Config.from_binary_to_map(config.value) + } + end +end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 47a73dc7e..a25f3f1fe 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.AdminAPI.ReportView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.HTML alias Pleroma.User alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView def render("index.json", %{reports: reports}) do @@ -23,6 +23,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do [account_ap_id | status_ap_ids] = report.data["object"] account = User.get_cached_by_ap_id(account_ap_id) + content = + unless is_nil(report.data["content"]) do + HTML.filter_tags(report.data["content"]) + else + nil + end + statuses = Enum.map(status_ap_ids, fn ap_id -> Activity.get_by_ap_id_with_object(ap_id) @@ -30,12 +37,19 @@ defmodule Pleroma.Web.AdminAPI.ReportView do %{ id: report.id, - account: AccountView.render("account.json", %{user: account}), - actor: AccountView.render("account.json", %{user: user}), - content: report.data["content"], + account: merge_account_views(account), + actor: merge_account_views(user), + content: content, created_at: created_at, statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), state: report.data["state"] } end + + defp merge_account_views(%User{} = user) do + Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user}) + |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) + end + + defp merge_account_views(_), do: %{} end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index c4a6fce08..a9164ad98 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -24,6 +24,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do end end + @doc """ + Gets or creates Pleroma.Registration record from Ueberauth assigns. + Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response — + see [`docs/config.md`](docs/config.md). + """ + def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}), + do: {:error, :missing_uid} + def get_registration(%Plug.Conn{ assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth} }) do @@ -51,9 +59,10 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials} + @doc "Creates Pleroma.User record basing on params and Pleroma.Registration record." def create_from_registration( %Plug.Conn{params: %{"authorization" => registration_attrs}}, - registration + %Registration{} = registration ) do nickname = value([registration_attrs["nickname"], Registration.nickname(registration)]) email = value([registration_attrs["email"], Registration.email(registration)]) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 25b5fedb8..8e3892bdf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -11,7 +11,9 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.ActivityPub.Visibility + import Pleroma.Web.Gettext import Pleroma.Web.CommonAPI.Utils def follow(follower, followed) do @@ -35,9 +37,9 @@ defmodule Pleroma.Web.CommonAPI do end def accept_follow_request(follower, followed) do - with {:ok, follower} <- User.maybe_follow(follower, followed), + with {:ok, follower} <- User.follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, _activity} <- ActivityPub.accept(%{ to: [follower.ap_id], @@ -51,7 +53,7 @@ defmodule Pleroma.Web.CommonAPI do def reject_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, _activity} <- ActivityPub.reject(%{ to: [follower.ap_id], @@ -73,7 +75,7 @@ defmodule Pleroma.Web.CommonAPI do {:ok, delete} else _ -> - {:error, "Could not delete"} + {:error, dgettext("errors", "Could not delete")} end end @@ -84,7 +86,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.announce(user, object) else _ -> - {:error, "Could not repeat"} + {:error, dgettext("errors", "Could not repeat")} end end @@ -94,7 +96,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.unannounce(user, object) else _ -> - {:error, "Could not unrepeat"} + {:error, dgettext("errors", "Could not unrepeat")} end end @@ -105,7 +107,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.like(user, object) else _ -> - {:error, "Could not favorite"} + {:error, dgettext("errors", "Could not favorite")} end end @@ -115,7 +117,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.unlike(user, object) else _ -> - {:error, "Could not unfavorite"} + {:error, dgettext("errors", "Could not unfavorite")} end end @@ -147,10 +149,10 @@ defmodule Pleroma.Web.CommonAPI do object = Object.get_cached_by_ap_id(object.data["id"]) {:ok, answer_activities, object} else - {:author, _} -> {:error, "Poll's author can't vote"} - {:existing_votes, _} -> {:error, "Already voted"} - {:choice_check, {_, false}} -> {:error, "Invalid indices"} - {:count_check, false} -> {:error, "Too many choices"} + {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")} + {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")} + {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")} + {:count_check, false} -> {:error, dgettext("errors", "Too many choices")} end end @@ -218,7 +220,7 @@ defmodule Pleroma.Web.CommonAPI do cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), full_payload <- String.trim(status <> cw), - length when length in 1..limit <- String.length(full_payload), + :ok <- validate_character_limit(full_payload, attachments, limit), object <- make_note_data( user.ap_id, @@ -250,7 +252,14 @@ defmodule Pleroma.Web.CommonAPI do Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false ) else - e -> {:error, e} + {:private_to_public, true} -> + {:error, dgettext("errors", "The message visibility must be direct")} + + {:error, _} = e -> + e + + e -> + {:error, e} end end @@ -285,12 +294,11 @@ defmodule Pleroma.Web.CommonAPI do }, object: %Object{ data: %{ - "to" => object_to, "type" => "Note" } } } = activity <- get_by_id_or_ap_id(id_or_ap_id), - true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"), + true <- Visibility.is_public?(activity), %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity), changeset <- @@ -302,7 +310,7 @@ defmodule Pleroma.Web.CommonAPI do {:error, err} _ -> - {:error, "Could not pin"} + {:error, dgettext("errors", "Could not pin")} end end @@ -319,7 +327,7 @@ defmodule Pleroma.Web.CommonAPI do {:error, err} _ -> - {:error, "Could not unpin"} + {:error, dgettext("errors", "Could not unpin")} end end @@ -327,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI do with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do {:ok, activity} else - {:error, _} -> {:error, "conversation is already muted"} + {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} end end @@ -372,8 +380,8 @@ defmodule Pleroma.Web.CommonAPI do {:ok, activity} else {:error, err} -> {:error, err} - {:account_id, %{}} -> {:error, "Valid `account_id` required"} - {:account, nil} -> {:error, "Account not found"} + {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")} + {:account, nil} -> {:error, dgettext("errors", "Account not found")} end end @@ -382,14 +390,9 @@ defmodule Pleroma.Web.CommonAPI do {:ok, activity} <- Utils.update_report_state(activity, state) do {:ok, activity} else - nil -> - {:error, :not_found} - - {:error, reason} -> - {:error, reason} - - _ -> - {:error, "Could not update state"} + nil -> {:error, :not_found} + {:error, reason} -> {:error, reason} + _ -> {:error, dgettext("errors", "Could not update state")} end end @@ -399,11 +402,8 @@ defmodule Pleroma.Web.CommonAPI do {:ok, activity} <- set_visibility(activity, opts) do {:ok, activity} else - nil -> - {:error, :not_found} - - {:error, reason} -> - {:error, reason} + nil -> {:error, :not_found} + {:error, reason} -> {:error, reason} end end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 6c9e117ae..d4bfdd7e4 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.CommonAPI.Utils do + import Pleroma.Web.Gettext + alias Calendar.Strftime alias Comeonin.Pbkdf2 alias Pleroma.Activity @@ -381,7 +383,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do true <- Pbkdf2.checkpw(password, db_user.password_hash) do {:ok, db_user} else - _ -> {:error, "Invalid password."} + _ -> {:error, dgettext("errors", "Invalid password.")} end end @@ -464,7 +466,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do if String.length(comment) <= max_size do {:ok, format_input(comment, "text/plain")} else - {:error, "Comment must be up to #{max_size} characters"} + {:error, + dgettext("errors", "Comment must be up to %{max_size} characters", max_size: max_size)} end end @@ -499,7 +502,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do context else _e -> - {:error, "No such conversation"} + {:error, dgettext("errors", "No such conversation")} end end @@ -513,4 +516,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do "inReplyTo" => object.data["id"] } end + + def validate_character_limit(full_payload, attachments, limit) do + length = String.length(full_payload) + + if length < limit do + if length > 0 or Enum.count(attachments) > 0 do + :ok + else + {:error, dgettext("errors", "Cannot post an empty status without attachments")} + end + else + {:error, dgettext("errors", "The status is over the character limit")} + end + end end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 55706eeb8..8a753bb4f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do |> put_status(status) |> json(json) end + + @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil + def fetch_integer_param(params, name, default \\ nil) do + params + |> Map.get(name, default) + |> param_to_integer(default) + end + + defp param_to_integer(val, _) when is_integer(val), do: val + + defp param_to_integer(val, default) when is_binary(val) do + case Integer.parse(val) do + {res, _} -> res + _ -> default + end + end + + defp param_to_integer(_, default), do: default end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index bd76e4295..c123530dc 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -7,13 +7,9 @@ defmodule Pleroma.Web.Endpoint do socket("/socket", Pleroma.Web.UserSocket) - # Serve at "/" the static files from "priv/static" directory. - # - # You should set gzip to true if you are running phoenix.digest - # when deploying your static files in production. + plug(Pleroma.Plugs.SetLocalePlug) plug(CORSPlug) plug(Pleroma.Plugs.HTTPSecurityPlug) - plug(Pleroma.Plugs.UploadedMedia) @static_cache_control "public, no-cache" @@ -30,6 +26,10 @@ defmodule Pleroma.Web.Endpoint do } ) + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phoenix.digest + # when deploying your static files in production. plug( Plug.Static, at: "/", @@ -91,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do Plug.Session, store: :cookie, key: cookie_name, - signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, + signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"), http_only: true, secure: secure_cookies, extra: extra diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index f4c9fe284..f4f9e83e0 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -22,6 +22,18 @@ defmodule Pleroma.Web.Federator do refresh_subscriptions() end + @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)" + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + def allowed_incoming_reply_depth?(depth) do + max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth]) + + if max_replies_depth do + (depth || 1) <= max_replies_depth + else + true + end + end + # Client API def incoming_doc(doc) do diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex index 71e49494f..3db948c2e 100644 --- a/lib/pleroma/web/federator/retry_queue.ex +++ b/lib/pleroma/web/federator/retry_queue.ex @@ -15,7 +15,9 @@ defmodule Pleroma.Web.Federator.RetryQueue do def start_link do enabled = - if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false) + if Pleroma.Config.get(:env) == :test, + do: true, + else: Pleroma.Config.get([__MODULE__, :enabled], false) if enabled do Logger.info("Starting retry queue") @@ -219,7 +221,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do {:noreply, state} end - if Mix.env() == :test do + if Pleroma.Config.get(:env) == :test do defp growth_function(_retries) do _shutit = Pleroma.Config.get([__MODULE__, :initial_timeout]) DateTime.to_unix(DateTime.utc_now()) - 1 diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 3a3ec7c2a..c82b20123 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.MastodonAPI do import Ecto.Query import Ecto.Changeset diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index d825555c6..8c2033c3a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.HTTP alias Pleroma.Notification alias Pleroma.Object - alias Pleroma.Object.Fetcher alias Pleroma.Pagination alias Pleroma.Repo alias Pleroma.ScheduledActivity @@ -47,14 +46,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger - plug( - Pleroma.Plugs.RateLimitPlug, - %{ - max_requests: Config.get([:app_account_creation, :max_requests]), - interval: Config.get([:app_account_creation, :interval]) - } - when action in [:account_register] - ) + plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register) + plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search]) @local_mastodon_name "Mastodon-Local" @@ -143,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do _ -> :error end end) + |> add_if_present(params, "pleroma_background_image", :background, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :background) do + {:ok, object.data} + else + _ -> :error + end + end) |> Map.put(:emoji, user_info_emojis) info_cng = User.Info.profile_update(user.info, info_params) @@ -159,16 +160,83 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) ) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Invalid request"}) + _e -> render_error(conn, :forbidden, "Invalid request") + end + end + + def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + change = Changeset.change(user, %{avatar: nil}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + + def update_avatar(%{assigns: %{user: user}} = conn, params) do + {:ok, object} = ActivityPub.upload(params, type: :avatar) + change = Changeset.change(user, %{avatar: object.data}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + + def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + with new_info <- %{"banner" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + end + + def update_banner(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), + new_info <- %{"banner" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + with new_info <- %{"background" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + json(conn, %{url: nil}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(params, type: :background), + new_info <- %{"background" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) end end def verify_credentials(%{assigns: %{user: user}} = conn, _) do + chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + account = - AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + AccountView.render("account.json", %{ + user: user, + for: user, + with_pleroma_settings: true, + with_chat_token: chat_token + }) json(conn, account) end @@ -187,10 +255,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do account = AccountView.render("account.json", %{user: user, for: for_user}) json(conn, account) else - _e -> - conn - |> put_status(404) - |> json(%{error: "Can't find user"}) + _e -> render_error(conn, :not_found, "Can't find user") end end @@ -234,7 +299,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do "static_url" => url, "visible_in_picker" => true, "url" => url, - "tags" => tags + "tags" => tags, + # Assuming that a comma is authorized in the category name + "category" => (tags -- ["Custom"]) |> Enum.join(",") } end) end @@ -348,6 +415,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do with %User{} = user <- User.get_cached_by_id(params["id"]) do + params = + params + |> Map.put("tag", params["tagged"]) + activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn @@ -434,41 +505,44 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> try_render("poll.json", %{object: object, for: user}) else - nil -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) - - false -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + nil -> render_error(conn, :not_found, "Record not found") + false -> render_error(conn, :not_found, "Record not found") end end + defp get_cached_vote_or_vote(user, object, choices) do + idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + + {_, res} = + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.vote(user, object, choices) do + {:error, _message} = res -> {:ignore, res} + res -> {:commit, res} + end + end) + + res + end + def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do with %Object{} = object <- Object.get_by_id(id), true <- object.data["type"] == "Question", %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), true <- Visibility.visible_for_user?(activity, user), - {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do + {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do conn |> put_view(StatusView) |> try_render("poll.json", %{object: object, for: user}) else nil -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + render_error(conn, :not_found, "Record not found") false -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + render_error(conn, :not_found, "Record not found") {:error, message} -> conn - |> put_status(422) + |> put_status(:unprocessable_entity) |> json(%{error: message}) end end @@ -522,15 +596,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params) - when length(media_ids) > 0 do - params = - params - |> Map.put("status", ".") - - post_status(conn, params) - end - def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do params = params @@ -548,18 +613,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else params = Map.drop(params, ["scheduled_at"]) - case get_cached_status_or_post(conn, params) do - {:ignore, message} -> - conn - |> put_status(422) - |> json(%{error: message}) - + case CommonAPI.post(user, params) do {:error, message} -> conn - |> put_status(422) + |> put_status(:unprocessable_entity) |> json(%{error: message}) - {_, activity} -> + {:ok, activity} -> conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -567,29 +627,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do - idempotency_key = - case get_req_header(conn, "idempotency-key") do - [key] -> key - _ -> Ecto.UUID.generate() - end - - Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> - case CommonAPI.post(user, params) do - {:ok, activity} -> activity - {:error, message} -> {:ignore, message} - end - end) - end - def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Can't delete this post"}) + _e -> render_error(conn, :forbidden, "Can't delete this post") end end @@ -637,8 +679,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) + |> put_status(:bad_request) + |> json(%{"error" => reason}) end end @@ -714,8 +756,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => reason})) + |> put_status(:forbidden) + |> json(%{"error" => reason}) end end @@ -730,8 +772,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => reason})) + |> put_status(:forbidden) + |> json(%{"error" => reason}) end end @@ -809,9 +851,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> json(rendered) else - conn - |> put_resp_content_type("application/json") - |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"})) + render_error(conn, :unsupported_media_type, "mascots can only be images") end end end @@ -831,7 +871,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(AccountView) - |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) else _ -> json(conn, []) end @@ -940,8 +980,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -954,8 +994,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -972,8 +1012,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -990,8 +1030,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1020,8 +1060,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1034,8 +1074,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1056,8 +1096,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1071,8 +1111,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1106,8 +1146,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1120,117 +1160,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) - end - end - - def status_search_query_with_gin(q, query) do - from([a, o] in q, - where: - fragment( - "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", - o.data, - ^query - ), - order_by: [desc: :id] - ) - end - - def status_search_query_with_rum(q, query) do - from([a, o] in q, - where: - fragment( - "? @@ plainto_tsquery('english', ?)", - o.fts_content, - ^query - ), - order_by: [fragment("? <=> now()::date", o.inserted_at)] - ) - end - - def status_search(user, query) do - fetched = - if Regex.match?(~r/https?:/, query) do - with {:ok, object} <- Fetcher.fetch_object_from_id(query), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), - true <- Visibility.visible_for_user?(activity, user) do - [activity] - else - _e -> [] - end - end || [] - - q = - from([a, o] in Activity.with_preloaded_object(Activity), - where: fragment("?->>'type' = 'Create'", a.data), - where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, - limit: 40 - ) - - q = - if Pleroma.Config.get([:database, :rum_enabled]) do - status_search_query_with_rum(q, query) - else - status_search_query_with_gin(q, query) - end - - Repo.all(q) ++ fetched - end - - def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - statuses = status_search(user, query) - - tags_path = Web.base_url() <> "/tag/" - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - statuses = status_search(user, query) - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - - json(conn, res) + |> put_status(:forbidden) + |> json(%{error: message}) + end end def favourites(%{assigns: %{user: user}} = conn, params) do @@ -1277,13 +1209,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> render("index.json", %{activities: activities, for: for_user, as: :activity}) else - nil -> - {:error, :not_found} - - true -> - conn - |> put_status(403) - |> json(%{error: "Can't get favorites"}) + nil -> {:error, :not_found} + true -> render_error(conn, :forbidden, "Can't get favorites") end end @@ -1315,10 +1242,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do res = ListView.render("list.json", list: list) json(conn, res) else - _e -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + _e -> render_error(conn, :not_found, "Record not found") end end @@ -1334,7 +1258,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1385,7 +1309,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, res) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1409,10 +1333,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> render("index.json", %{activities: activities, for: user, as: :activity}) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Error."}) + _e -> render_error(conn, :forbidden, "Error.") end end @@ -1531,8 +1452,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else e -> conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) + |> put_status(:internal_server_error) + |> json(%{error: inspect(e)}) end end @@ -1700,20 +1621,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> Enum.map_join(", ", fn {_k, v} -> v end) conn - |> put_status(422) + |> put_status(:unprocessable_entity) |> json(%{error: error_message}) end def errors(conn, {:error, :not_found}) do - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + render_error(conn, :not_found, "Record not found") end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end def suggestions(%{assigns: %{user: user}} = conn, _) do @@ -1833,21 +1752,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, errors} -> conn - |> put_status(400) - |> json(Jason.encode!(errors)) + |> put_status(:bad_request) + |> json(errors) end end def account_register(%{assigns: %{app: _app}} = conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Missing parameters"}) + render_error(conn, :bad_request, "Missing parameters") end def account_register(conn, _) do - conn - |> put_status(403) - |> json(%{error: "Invalid credentials"}) + render_error(conn, :forbidden, "Invalid credentials") end def conversations(%{assigns: %{user: user}} = conn, params) do @@ -1877,21 +1792,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def try_render(conn, target, params) when is_binary(target) do - res = render(conn, target, params) - - if res == nil do - conn - |> put_status(501) - |> json(%{error: "Can't display this activity"}) - else - res + case render(conn, target, params) do + nil -> render_error(conn, :not_implemented, "Can't display this activity") + res -> res end end def try_render(conn, _, _) do - conn - |> put_status(501) - |> json(%{error: "Can't display this activity"}) + render_error(conn, :not_implemented, "Can't display this activity") end defp present?(nil), do: false diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex new file mode 100644 index 000000000..939f7f6cb --- /dev/null +++ b/lib/pleroma/web/mastodon_api/search_controller.ex @@ -0,0 +1,90 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Plugs.RateLimiter + alias Pleroma.User + alias Pleroma.Web + alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + require Logger + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + + def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = with_fallback(fn -> User.search(query, search_options(params, user)) end, []) + statuses = with_fallback(fn -> Activity.search(user, query) end, []) + + tags_path = Web.base_url() <> "/tag/" + + tags = + query + |> prepare_tags + |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) + + res = %{ + "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), + "statuses" => + StatusView.render("index.json", activities: statuses, for: user, as: :activity), + "hashtags" => tags + } + + json(conn, res) + end + + def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = with_fallback(fn -> User.search(query, search_options(params, user)) end) + statuses = with_fallback(fn -> Activity.search(user, query) end) + + tags = prepare_tags(query) + + res = %{ + "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), + "statuses" => + StatusView.render("index.json", activities: statuses, for: user, as: :activity), + "hashtags" => tags + } + + json(conn, res) + end + + def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, search_options(params, user)) + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + + json(conn, res) + end + + defp prepare_tags(query) do + query + |> String.split() + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) + end + + defp search_options(params, user) do + [ + resolve: params["resolve"] == "true", + following: params["following"] == "true", + limit: ControllerHelper.fetch_integer_param(params, "limit"), + offset: ControllerHelper.fetch_integer_param(params, "offset"), + for_user: user + ] + end + + defp with_fallback(f, fallback \\ []) do + try do + f.() + rescue + error -> + Logger.error("#{__MODULE__} search error: #{inspect(error)}") + fallback + end + end +end diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/subscription_controller.ex index b6c8ff808..255ee2f18 100644 --- a/lib/pleroma/web/mastodon_api/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/subscription_controller.ex @@ -59,13 +59,13 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do # def errors(conn, {:error, :not_found}) do conn - |> put_status(404) - |> json("Not found") + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b91726b45..62c516f8e 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -66,6 +66,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end defp do_render("account.json", %{user: user} = opts) do + display_name = HTML.strip_tags(user.name || user.nickname) + image = User.avatar_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url() user_info = User.get_cached_user_info(user) @@ -96,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do id: to_string(user.id), username: username_from_nickname(user.nickname), acct: user.nickname, - display_name: user.name || user.nickname, + display_name: display_name, locked: user_info.locked, created_at: Utils.to_masto_date(user.inserted_at), followers_count: user_info.follower_count, @@ -125,13 +127,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do hide_follows: user.info.hide_follows, hide_favorites: user.info.hide_favorites, relationship: relationship, - skip_thread_containment: user.info.skip_thread_containment + skip_thread_containment: user.info.skip_thread_containment, + background_image: image_url(user.info.background) |> MediaProxy.url() } } |> maybe_put_role(user, opts[:for]) |> maybe_put_settings(user, opts[:for], user_info) |> maybe_put_notification_settings(user, opts[:for]) |> maybe_put_settings_store(user, opts[:for], opts) + |> maybe_put_chat_token(user, opts[:for], opts) end defp username_from_nickname(string) when is_binary(string) do @@ -163,6 +167,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_settings_store(data, _, _, _), do: data + defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{ + with_chat_token: token + }) do + data + |> Kernel.put_in([:pleroma, :chat_token], token) + end + + defp maybe_put_chat_token(data, _, _, _), do: data + defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do data |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) @@ -182,4 +195,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end defp maybe_put_notification_settings(data, _, _), do: data + + defp image_url(%{"url" => [%{"href" => href} | _]}), do: href + defp image_url(_), do: nil end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index af1dcf66d..38bdec737 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.ConversationView do use Pleroma.Web, :view diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 6836d331a..06a7251d8 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] # TODO: Add cached version. + defp get_replied_to_activities([]), do: %{} + defp get_replied_to_activities(activities) do activities |> Enum.map(fn @@ -104,7 +106,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do id: to_string(activity.id), uri: activity_object.data["id"], url: activity_object.data["id"], - account: AccountView.render("account.json", %{user: user}), + account: AccountView.render("account.json", %{user: user, for: opts[:for]}), in_reply_to_id: nil, in_reply_to_account_id: nil, reblog: reblogged, @@ -147,8 +149,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do tags = object.data["tag"] || [] sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw") + tag_mentions = + tags + |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end) + |> Enum.map(fn tag -> tag["href"] end) + mentions = - activity.recipients + (object.data["to"] ++ tag_mentions) + |> Enum.uniq() |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) |> Enum.filter(& &1) |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) @@ -221,7 +229,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do id: to_string(activity.id), uri: object.data["id"], url: url, - account: AccountView.render("account.json", %{user: user}), + account: AccountView.render("account.json", %{user: user, for: opts[:for]}), in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), reblog: nil, diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index abfa26754..dbd3542ea 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do "public:media", "public:local:media", "user", + "user:notification", "direct", "list", "hashtag" @@ -28,9 +29,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do def init(%{qs: qs} = req, state) do with params <- :cow_qs.parse_qs(qs), + sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil), access_token <- List.keyfind(params, "access_token", 0), {_, stream} <- List.keyfind(params, "stream", 0), - {:ok, user} <- allow_request(stream, access_token), + {:ok, user} <- allow_request(stream, [access_token, sec_websocket]), topic when is_binary(topic) <- expand_topic(stream, params) do {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}} else @@ -83,13 +85,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do end # Public streams without authentication. - defp allow_request(stream, nil) when stream in @anonymous_streams do + defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do {:ok, nil} end # Authenticated streams. - defp allow_request(stream, {"access_token", access_token}) when stream in @streams do - with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), + defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do + token = + with {"access_token", token} <- access_token do + token + else + _ -> sec_websocket + end + + with true <- is_bitstring(token), + %Token{user_id: user_id} <- Repo.get_by(Token, token: token), user = %User{} <- User.get_cached_by_id(user_id) do {:ok, user} else diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index cee6d8481..dd8888a02 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -33,20 +33,7 @@ defmodule Pleroma.Web.MediaProxy do def encode_url(url) do secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) - - # Must preserve `%2F` for compatibility with S3 - # https://git.pleroma.social/pleroma/pleroma/issues/580 - replacement = get_replacement(url, ":2F:") - - # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. - base64 = - url - |> String.replace("%2F", replacement) - |> URI.decode() - |> URI.encode() - |> String.replace(replacement, "%2F") - |> Base.url_encode64(@base64_opts) - + base64 = Base.url_encode64(url, @base64_opts) sig = :crypto.hmac(:sha, secret, base64) sig64 = sig |> Base.url_encode64(@base64_opts) @@ -80,12 +67,4 @@ defmodule Pleroma.Web.MediaProxy do |> Enum.filter(fn value -> value end) |> Path.join() end - - defp get_replacement(url, replacement) do - if String.contains?(url, replacement) do - get_replacement(url, replacement <> replacement) - else - replacement - end - end end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 357b80a2d..4033ec38f 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -121,4 +121,6 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do acc ++ rendered_tags end) end + + defp build_attachments(_), do: [] end diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex index e9a8cfc8d..4289ebdbd 100644 --- a/lib/pleroma/web/metadata/player_view.ex +++ b/lib/pleroma/web/metadata/player_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Metadata.PlayerView do use Pleroma.Web, :view import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2] diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex index 03af899c4..f87fc1973 100644 --- a/lib/pleroma/web/metadata/rel_me.ex +++ b/lib/pleroma/web/metadata/rel_me.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Metadata.Providers.RelMe do alias Pleroma.Web.Metadata.Providers.Provider @behaviour Provider diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 040b872e7..8dd01e0d5 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -117,6 +117,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end) end + defp build_attachments(_id, _object), do: [] + defp player_url(id) do Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 489d5d3a5..b786a521b 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do else false -> conn - |> put_status(403) + |> put_status(:forbidden) |> json(false) _ -> diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 32be430b7..cd9a4f4a8 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -162,7 +162,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do accountActivationRequired: Config.get([:instance, :account_activation_required], false), invitesEnabled: Config.get([:instance, :invites_enabled], false), features: features, - restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]) + restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), + skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) } } end @@ -200,8 +201,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do end def nodeinfo(conn, _) do - conn - |> put_status(404) - |> json(%{error: "Nodeinfo schema version not handled"}) + render_error(conn, :not_found, "Nodeinfo schema version not handled") end end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 18973413e..d53e20d12 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -76,14 +76,16 @@ defmodule Pleroma.Web.OAuth.Authorization do def use_token(%Authorization{used: true}), do: {:error, "already used"} @spec delete_user_authorizations(User.t()) :: {integer(), any()} - def delete_user_authorizations(%User{id: user_id}) do - from( - a in Pleroma.Web.OAuth.Authorization, - where: a.user_id == ^user_id - ) + def delete_user_authorizations(%User{} = user) do + user + |> delete_by_user_query |> Repo.delete_all() end + def delete_by_user_query(%User{id: user_id}) do + from(a in __MODULE__, where: a.user_id == ^user_id) + end + @doc "gets auth for app by token" @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} def get_by_token(%App{id: app_id} = _app, token) do diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index e3984f009..dd7f08bf1 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -9,21 +9,24 @@ defmodule Pleroma.Web.OAuth.FallbackController do def call(conn, {:register, :generic_error}) do conn |> put_status(:internal_server_error) - |> put_flash(:error, "Unknown error, please check the details and try again.") + |> put_flash( + :error, + dgettext("errors", "Unknown error, please check the details and try again.") + ) |> OAuthController.registration_details(conn.params) end def call(conn, {:register, _error}) do conn |> put_status(:unauthorized) - |> put_flash(:error, "Invalid Username/Password") + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) |> OAuthController.registration_details(conn.params) end def call(conn, _error) do conn |> put_status(:unauthorized) - |> put_flash(:error, "Invalid Username/Password") + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) |> OAuthController.authorize(conn.params) end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ae2b80d95..ef53b7ae3 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller + alias Pleroma.Helpers.UriHelper alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User @@ -17,6 +18,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken alias Pleroma.Web.OAuth.Scopes + require Logger + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) plug(:fetch_session) @@ -24,34 +27,25 @@ defmodule Pleroma.Web.OAuth.OAuthController do action_fallback(Pleroma.Web.OAuth.FallbackController) + @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg - def authorize(conn, %{"authorization" => _} = params) do + def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do {auth_attrs, params} = Map.pop(params, "authorization") authorize(conn, Map.merge(params, auth_attrs)) end - def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do + def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do if ControllerHelper.truthy_param?(params["force_login"]) do do_authorize(conn, params) else - redirect_uri = - if is_binary(params["redirect_uri"]) do - params["redirect_uri"] - else - app = Repo.preload(token, :app).app - - app.redirect_uris - |> String.split() - |> Enum.at(0) - end - - redirect(conn, external: redirect_uri(conn, redirect_uri)) + handle_existing_authorization(conn, params) end end - def authorize(conn, params), do: do_authorize(conn, params) + def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) - defp do_authorize(conn, params) do + defp do_authorize(%Plug.Conn{} = conn, params) do app = Repo.get_by(App, client_id: params["client_id"]) available_scopes = (app && app.scopes) || [] scopes = Scopes.fetch_scopes(params, available_scopes) @@ -68,8 +62,41 @@ defmodule Pleroma.Web.OAuth.OAuthController do }) end + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"redirect_uri" => @oob_token_redirect_uri} + ) do + render(conn, "oob_token_exists.html", %{token: token}) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{} = params + ) do + app = Repo.preload(token, :app).app + + redirect_uri = + if is_binary(params["redirect_uri"]) do + params["redirect_uri"] + else + default_redirect_uri(app) + end + + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{access_token: token.token} + url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) + url = UriHelper.append_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + def create_authorization( - conn, + %Plug.Conn{} = conn, %{"authorization" => _} = params, opts \\ [] ) do @@ -81,35 +108,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def after_create_authorization(conn, auth, %{ - "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} }) do - redirect_uri = redirect_uri(conn, redirect_uri) - - if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do - render(conn, "results.html", %{ - auth: auth - }) - else - connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" - url = "#{redirect_uri}#{connector}" - url_params = %{:code => auth.token} - - url_params = - if auth_attrs["state"] do - Map.put(url_params, :state, auth_attrs["state"]) - else - url_params - end - - url = "#{url}#{Plug.Conn.Query.encode(url_params)}" + render(conn, "oob_authorization_created.html", %{auth: auth}) + end + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do + app = Repo.preload(auth, :app).app + + # An extra safety measure before we redirect (also done in `do_create_authorization/2`) + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{code: auth.token} + url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) end end defp handle_create_authorization_error( - conn, + %Plug.Conn{} = conn, {:error, scopes_issue}, %{"authorization" => _} = params ) @@ -117,31 +142,31 @@ defmodule Pleroma.Web.OAuth.OAuthController do # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 conn - |> put_flash(:error, "This action is outside the authorized scopes") + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) |> put_status(:unauthorized) |> authorize(params) end defp handle_create_authorization_error( - conn, + %Plug.Conn{} = conn, {:auth_active, false}, %{"authorization" => _} = params ) do # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 conn - |> put_flash(:error, "Your login is missing a confirmed e-mail address") + |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) |> put_status(:forbidden) |> authorize(params) end - defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do + defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do Authenticator.handle_error(conn, error) end @doc "Renew access_token with refresh_token" def token_exchange( - conn, + %Plug.Conn{} = conn, %{"grant_type" => "refresh_token", "refresh_token" => token} = _params ) do with {:ok, app} <- Token.Utils.fetch_app(conn), @@ -151,13 +176,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do json(conn, Token.Response.build(user, token, response_attrs)) else - _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + _error -> render_invalid_credentials_error(conn) end end - def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do with {:ok, app} <- Token.Utils.fetch_app(conn), fixed_token = Token.Utils.fix_padding(params["code"]), {:ok, auth} <- Authorization.get_by_token(app, fixed_token), @@ -167,14 +190,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do json(conn, Token.Response.build(user, token, response_attrs)) else - _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + _error -> render_invalid_credentials_error(conn) end end def token_exchange( - conn, + %Plug.Conn{} = conn, %{"grant_type" => "password"} = params ) do with {:ok, %User{} = user} <- Authenticator.get_user(conn), @@ -189,23 +210,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do {:auth_active, false} -> # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 - conn - |> put_status(:forbidden) - |> json(%{error: "Your login is missing a confirmed e-mail address"}) + render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address") {:user_active, false} -> - conn - |> put_status(:forbidden) - |> json(%{error: "Your account is currently disabled"}) + render_error(conn, :forbidden, "Your account is currently disabled") _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + render_invalid_credentials_error(conn) end end def token_exchange( - conn, + %Plug.Conn{} = conn, %{"grant_type" => "password", "name" => name, "password" => _password} = params ) do params = @@ -216,22 +232,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do token_exchange(conn, params) end - def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, Token.Response.build_for_client_credentials(token)) else - _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + _error -> render_invalid_credentials_error(conn) end end # Bad request - def token_exchange(conn, params), do: bad_request(conn, params) + def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - def token_revoke(conn, %{"token" => _token} = params) do + def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, _token} <- RevokeToken.revoke(app, params) do json(conn, %{}) @@ -242,17 +256,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def token_revoke(conn, params), do: bad_request(conn, params) + def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) # Response for bad request - defp bad_request(conn, _) do - conn - |> put_status(500) - |> json(%{error: "Bad request"}) + defp bad_request(%Plug.Conn{} = conn, _) do + render_error(conn, :internal_server_error, "Bad request") end @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do + def prepare_request(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do scope = auth_attrs |> Scopes.fetch_scopes([]) @@ -273,12 +288,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do redirect(conn, to: o_auth_path(conn, :request, provider, params)) end - def request(conn, params) do + def request(%Plug.Conn{} = conn, params) do message = if params["provider"] do - "Unsupported OAuth provider: #{params["provider"]}." + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) else - "Bad OAuth request." + dgettext("errors", "Bad OAuth request.") end conn @@ -286,17 +303,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do |> redirect(to: "/") end - def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do + def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do params = callback_params(params) messages = for e <- Map.get(failure, :errors, []), do: e.message message = Enum.join(messages, "; ") conn - |> put_flash(:error, "Failed to authenticate: #{message}.") + |> put_flash( + :error, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) |> redirect(external: redirect_uri(conn, params["redirect_uri"])) end - def callback(conn, params) do + def callback(%Plug.Conn{} = conn, params) do params = callback_params(params) with {:ok, registration} <- Authenticator.get_registration(conn) do @@ -314,13 +334,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do }) conn - |> put_session(:registration_id, registration.id) + |> put_session_registration_id(registration.id) |> registration_details(%{"authorization" => registration_params}) end else - _ -> + error -> + Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) + conn - |> put_flash(:error, "Failed to set up user account.") + |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) |> redirect(external: redirect_uri(conn, params["redirect_uri"])) end end @@ -329,7 +351,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do Map.merge(params, Jason.decode!(state)) end - def registration_details(conn, %{"authorization" => auth_attrs}) do + def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do render(conn, "register.html", %{ client_id: auth_attrs["client_id"], redirect_uri: auth_attrs["redirect_uri"], @@ -340,7 +362,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do }) end - def register(conn, %{"authorization" => _, "op" => "connect"} = params) do + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), {_, {:ok, auth}} <- @@ -359,7 +381,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def register(conn, %{"authorization" => _, "op" => "register"} = params) do + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), {:ok, user} <- Authenticator.create_from_registration(conn, registration) do @@ -395,7 +417,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do end defp do_create_authorization( - conn, + %Plug.Conn{} = conn, %{ "authorization" => %{ @@ -416,13 +438,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do end # Special case: Local MastodonFE - defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) + defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login) - defp redirect_uri(_conn, redirect_uri), do: redirect_uri + defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - defp get_session_registration_id(conn), do: get_session(conn, :registration_id) + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - defp put_session_registration_id(conn, registration_id), + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) @spec validate_scopes(App.t(), map()) :: @@ -432,4 +454,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do |> Scopes.fetch_scopes(app.scopes) |> Scopes.validates(app.scopes) end + + def default_redirect_uri(%App{} = app) do + app.redirect_uris + |> String.split() + |> Enum.at(0) + end + + defp render_invalid_credentials_error(conn) do + render_error(conn, :bad_request, "Invalid credentials") + end end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f412f7eb2..90c304487 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.OAuth.Token do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Query - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) @type t :: %__MODULE__{} schema "oauth_tokens" do @@ -78,7 +77,7 @@ defmodule Pleroma.Web.OAuth.Token do defp put_valid_until(changeset, attrs) do expires_in = - Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) changeset |> change(%{valid_until: expires_in}) @@ -123,4 +122,6 @@ defmodule Pleroma.Web.OAuth.Token do end def is_expired?(_), do: false + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex index 64e78b183..266110814 100644 --- a/lib/pleroma/web/oauth/token/response.ex +++ b/lib/pleroma/web/oauth/token/response.ex @@ -1,18 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Response do @moduledoc false alias Pleroma.User alias Pleroma.Web.OAuth.Token.Utils - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) - @doc false def build(%User{} = user, token, opts \\ %{}) do %{ token_type: "Bearer", access_token: token.token, refresh_token: token.refresh_token, - expires_in: @expires_in, + expires_in: expires_in(), scope: Enum.join(token.scopes, " "), me: user.ap_id } @@ -25,8 +27,10 @@ defmodule Pleroma.Web.OAuth.Token.Response do access_token: token.token, refresh_token: token.refresh_token, created_at: Utils.format_created_at(token), - expires_in: @expires_in, + expires_in: expires_in(), scope: Enum.join(token.scopes, " ") } end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex index 7df0be14e..c620050c8 100644 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do @moduledoc """ Functions for dealing with refresh token strategy. diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex index dea63ca54..983f095b4 100644 --- a/lib/pleroma/web/oauth/token/strategy/revoke.ex +++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do @moduledoc """ Functions for dealing with revocation. diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex index 7a4fddafd..1e8765e93 100644 --- a/lib/pleroma/web/oauth/token/utils.ex +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Utils do @moduledoc """ Auxiliary functions for dealing with tokens. diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index ec6e5cfaf..8e0adad91 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Federator alias Pleroma.Web.OStatus alias Pleroma.Web.XML @@ -88,14 +89,15 @@ defmodule Pleroma.Web.OStatus.NoteHandler do Map.put(note, "external_url", url) end - def fetch_replied_to_activity(entry, in_reply_to) do + def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do activity else _e -> - with in_reply_to_href when not is_nil(in_reply_to_href) <- + with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), + in_reply_to_href when not is_nil(in_reply_to_href) <- XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry), - {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do + {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do activity else _e -> nil @@ -104,7 +106,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do end # TODO: Clean this up a bit. - def handle_note(entry, doc \\ nil) do + def handle_note(entry, doc \\ nil, options \\ []) do with id <- XML.string_from_xpath("//id", entry), activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id), [author] <- :xmerl_xpath.string('//author[1]', doc), @@ -112,7 +114,8 @@ defmodule Pleroma.Web.OStatus.NoteHandler do content_html <- OStatus.get_content(entry), cw <- OStatus.get_cw(entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), - in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to), + options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1), + in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options), in_reply_to_object <- (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil, in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to, diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 6ed089d84..502410c83 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -54,7 +54,7 @@ defmodule Pleroma.Web.OStatus do "#{Web.base_url()}/ostatus_subscribe?acct={uri}" end - def handle_incoming(xml_string) do + def handle_incoming(xml_string, options \\ []) do with doc when doc != :error <- parse_document(xml_string) do with {:ok, actor_user} <- find_make_or_update_user(doc), do: Pleroma.Instances.set_reachable(actor_user.ap_id) @@ -91,10 +91,12 @@ defmodule Pleroma.Web.OStatus do _ -> case object_type do 'http://activitystrea.ms/schema/1.0/note' -> - with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity + with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), + do: activity 'http://activitystrea.ms/schema/1.0/comment' -> - with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity + with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), + do: activity _ -> Logger.error("Couldn't parse incoming document") @@ -359,7 +361,7 @@ defmodule Pleroma.Web.OStatus do end end - def fetch_activity_from_atom_url(url) do + def fetch_activity_from_atom_url(url, options \\ []) do with true <- String.starts_with?(url, "http"), {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get( @@ -367,7 +369,7 @@ defmodule Pleroma.Web.OStatus do [{:Accept, "application/atom+xml"}] ) do Logger.debug("Got document from #{url}, handling...") - handle_incoming(body) + handle_incoming(body, options) else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}") @@ -375,13 +377,13 @@ defmodule Pleroma.Web.OStatus do end end - def fetch_activity_from_html_url(url) do + def fetch_activity_from_html_url(url, options \\ []) do Logger.debug("Trying to fetch #{url}") with true <- String.starts_with?(url, "http"), {:ok, %{body: body}} <- HTTP.get(url, []), {:ok, atom_url} <- get_atom_url(body) do - fetch_activity_from_atom_url(atom_url) + fetch_activity_from_atom_url(atom_url, options) else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}") @@ -389,11 +391,11 @@ defmodule Pleroma.Web.OStatus do end end - def fetch_activity_from_url(url) do - with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do + def fetch_activity_from_url(url, options \\ []) do + with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do {:ok, activities} else - _e -> fetch_activity_from_html_url(url) + _e -> fetch_activity_from_html_url(url, options) end rescue e -> diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 2fb6ce41b..372d52899 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -245,14 +245,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do end def errors(conn, {:error, :not_found}) do - conn - |> put_status(404) - |> text("Not found") + render_error(conn, :not_found, "Not found") end def errors(conn, _) do - conn - |> put_status(500) - |> text("Something went wrong") + render_error(conn, :internal_server_error, "Something went wrong") end end diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 26eb614a6..d376e2069 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.RelMe do with_body: true ] - if Mix.env() == :test do + if Pleroma.Config.get(:env) == :test do def parse(url) when is_binary(url), do: parse_url(url) else def parse(url) when is_binary(url) do diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 9bc8f2559..6506de46c 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -4,25 +4,53 @@ defmodule Pleroma.Web.RichMedia.Helpers do alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Web.RichMedia.Parser + @spec validate_page_url(any()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do - if AutoLinker.Parser.is_url?(page_url, true) do - URI.parse(page_url) |> validate_page_url - else - :error + validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld] + + page_url + |> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld) + |> parse_uri(page_url) + end + + defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority}) + when scheme == "https" and not is_nil(authority) do + cond do + host in Config.get([:rich_media, :ignore_hosts], []) -> + :error + + get_tld(host) in Config.get([:rich_media, :ignore_tld], []) -> + :error + + true -> + :ok end end - defp validate_page_url(%URI{authority: nil}), do: :error - defp validate_page_url(%URI{scheme: nil}), do: :error - defp validate_page_url(%URI{}), do: :ok defp validate_page_url(_), do: :error + defp parse_uri(true, url) do + url + |> URI.parse() + |> validate_page_url + end + + defp parse_uri(_, _), do: :error + + defp get_tld(host) do + host + |> String.split(".") + |> Enum.reverse() + |> hd + end + def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do - with true <- Pleroma.Config.get([:rich_media, :enabled]), + with true <- Config.get([:rich_media, :enabled]), %Object{} = object <- Object.normalize(activity), false <- object.data["sensitive"] || false, {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index e4595800c..21cd47890 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.RichMedia.Parser do def parse(nil), do: {:error, "No URL provided"} - if Mix.env() == :test do + if Pleroma.Config.get(:env) == :test do def parse(url), do: parse_url(url) else def parse(url) do diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index 4a7c5eae0..913975616 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -1,15 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do - with elements = [_ | _] <- get_elements(html, key_name, prefix), - meta_data = - Enum.reduce(elements, data, fn el, acc -> - attributes = normalize_attributes(el, prefix, key_name, value_name) + meta_data = + html + |> get_elements(key_name, prefix) + |> Enum.reduce(data, fn el, acc -> + attributes = normalize_attributes(el, prefix, key_name, value_name) - Map.merge(acc, attributes) - end) do - {:ok, meta_data} + Map.merge(acc, attributes) + end) + |> maybe_put_title(html) + + if Enum.empty?(meta_data) do + {:error, error_message} else - _e -> {:error, error_message} + {:ok, meta_data} end end @@ -27,4 +35,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do %{String.to_atom(data[key_name]) => data[value_name]} end + + defp maybe_put_title(%{title: _} = meta, _), do: meta + + defp maybe_put_title(meta, html) when meta != %{} do + case get_page_title(html) do + "" -> meta + title -> Map.put_new(meta, :title, title) + end + end + + defp maybe_put_title(meta, _), do: meta + + defp get_page_title(html) do + Floki.find(html, "title") |> Floki.text() + end end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex index 2530b8c9d..875637c4d 100644 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do def parse(html, _data) do with elements = [_ | _] <- get_discovery_data(html), diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex index 0e1a0e719..d40fa009f 100644 --- a/lib/pleroma/web/rich_media/parsers/ogp.ex +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.OGP do def parse(html, data) do Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex index a317c3e78..e4efe2dd0 100644 --- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex +++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do def parse(html, data) do Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e699f6ae2..d53fa8a35 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.UserEnabledPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug) + plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :authenticated_api do @@ -41,6 +42,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.UserEnabledPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) + plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :admin_api do @@ -57,6 +59,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.UserIsAdminPlug) + plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :mastodon_html do @@ -133,8 +136,8 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma", Pleroma.Web.TwitterAPI do pipe_through(:pleroma_api) - get("/password_reset/:token", UtilController, :show_password_reset) - post("/password_reset", UtilController, :password_reset) + get("/password_reset/:token", PasswordController, :reset, as: :reset_password) + post("/password_reset", PasswordController, :do_reset, as: :reset_password) get("/emoji", UtilController, :emoji) get("/captcha", UtilController, :captcha) get("/healthcheck", UtilController, :healthcheck) @@ -202,6 +205,9 @@ defmodule Pleroma.Web.Router do put("/statuses/:id", AdminAPIController, :status_update) delete("/statuses/:id", AdminAPIController, :status_delete) + + get("/config", AdminAPIController, :config_show) + post("/config", AdminAPIController, :config_update) end scope "/", Pleroma.Web.TwitterAPI do @@ -316,6 +322,10 @@ defmodule Pleroma.Web.Router do patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) + patch("/accounts/update_avatar", MastodonAPIController, :update_avatar) + patch("/accounts/update_banner", MastodonAPIController, :update_banner) + patch("/accounts/update_background", MastodonAPIController, :update_background) + post("/statuses", MastodonAPIController, :post_status) delete("/statuses/:id", MastodonAPIController, :delete_status) @@ -412,7 +422,7 @@ defmodule Pleroma.Web.Router do get("/trends", MastodonAPIController, :empty_array) - get("/accounts/search", MastodonAPIController, :account_search) + get("/accounts/search", SearchController, :account_search) scope [] do pipe_through(:oauth_read_or_public) @@ -431,7 +441,7 @@ defmodule Pleroma.Web.Router do get("/accounts/:id/following", MastodonAPIController, :following) get("/accounts/:id", MastodonAPIController, :user) - get("/search", MastodonAPIController, :search) + get("/search", SearchController, :search) get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) end @@ -439,7 +449,7 @@ defmodule Pleroma.Web.Router do scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through([:api, :oauth_read_or_public]) - get("/search", MastodonAPIController, :search2) + get("/search", SearchController, :search2) end scope "/api", Pleroma.Web do @@ -604,12 +614,6 @@ defmodule Pleroma.Web.Router do post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) end - scope "/", Pleroma.Web do - pipe_through(:oembed) - - get("/oembed", OEmbed.OEmbedController, :url) - end - pipeline :activitypub do plug(:accepts, ["activity+json", "json"]) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -699,7 +703,7 @@ defmodule Pleroma.Web.Router do get("/:sig/:url/:filename", MediaProxyController, :remote) end - if Mix.env() == :dev do + if Pleroma.Config.get(:env) == :dev do scope "/dev" do pipe_through([:mailbox_preview]) @@ -724,6 +728,7 @@ end defmodule Fallback.RedirectController do use Pleroma.Web, :controller + require Logger alias Pleroma.User alias Pleroma.Web.Metadata @@ -750,7 +755,20 @@ defmodule Fallback.RedirectController do def redirector_with_meta(conn, params) do {:ok, index_content} = File.read(index_file_path()) - tags = Metadata.build_tags(params) + + tags = + try do + Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + response = String.replace(index_content, "<!--server-generated-meta-->", tags) conn diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 19e3ef401..9b01ebcc6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -161,7 +161,7 @@ defmodule Pleroma.Web.Salmon do do: Instances.set_reachable(url) Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) - :ok + {:ok, code} else e -> unless params[:unreachable_since], do: Instances.set_reachable(url) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index a23f80f26..4f325113a 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -110,23 +110,18 @@ defmodule Pleroma.Web.Streamer do {:noreply, topics} end - def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do - topic = "user:#{item.user_id}" - - Enum.each(topics[topic] || [], fn socket -> - json = - %{ - event: "notification", - payload: - NotificationView.render("show.json", %{ - notification: item, - for: socket.assigns["user"] - }) - |> Jason.encode!() - } - |> Jason.encode!() - - send(socket.transport_pid, {:text, json}) + def handle_cast( + %{action: :stream, topic: topic, item: %Notification{} = item}, + topics + ) + when topic in ["user", "user:notification"] do + topics + |> Map.get("#{topic}:#{item.user_id}", []) + |> Enum.each(fn socket -> + send( + socket.transport_pid, + {:text, represent_notification(socket.assigns[:user], item)} + ) end) {:noreply, topics} @@ -216,6 +211,20 @@ defmodule Pleroma.Web.Streamer do |> Jason.encode!() end + @spec represent_notification(User.t(), Notification.t()) :: binary() + defp represent_notification(%User{} = user, %Notification{} = notify) do + %{ + event: "notification", + payload: + NotificationView.render( + "show.json", + %{notification: notify, for: user} + ) + |> Jason.encode!() + } + |> Jason.encode!() + end + def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do Enum.each(topics[topic] || [], fn socket -> # Get the current user so we have up-to-date blocks etc. @@ -274,7 +283,7 @@ defmodule Pleroma.Web.Streamer do end) end - defp internal_topic(topic, socket) when topic in ~w[user direct] do + defp internal_topic(topic, socket) when topic in ~w[user user:notification direct] do "#{topic}:#{socket.assigns[:user].id}" end diff --git a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex index 8443d906b..8443d906b 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex new file mode 100644 index 000000000..961aad976 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex @@ -0,0 +1,2 @@ +<h1>Authorization exists</h1> +<h2>Access token is <%= @token.token %></h2> diff --git a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex index ee84750c7..ee84750c7 100644 --- a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex index a3facf017..7d3ef6b0d 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex @@ -1,5 +1,5 @@ <h2>Password Reset for <%= @user.nickname %></h2> -<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %> +<%= form_for @conn, reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %> <div class="form-row"> <%= label f, :password, "Password" %> <%= password_input f, :password %> diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex index df037c01e..df037c01e 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex index f30ba3274..f30ba3274 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex diff --git a/lib/pleroma/web/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex new file mode 100644 index 000000000..8f5a43bf6 --- /dev/null +++ b/lib/pleroma/web/translation_helpers.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TranslationHelpers do + defmacro render_error(conn, status, msgid, bindings \\ Macro.escape(%{})) do + quote do + require Pleroma.Web.Gettext + + unquote(conn) + |> Plug.Conn.put_status(unquote(status)) + |> Phoenix.Controller.json(%{ + error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)) + }) + end + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex new file mode 100644 index 000000000..1941e6143 --- /dev/null +++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordController do + @moduledoc """ + The module containts functions for reset password. + """ + + use Pleroma.Web, :controller + + require Logger + + alias Pleroma.PasswordResetToken + alias Pleroma.Repo + alias Pleroma.User + + def reset(conn, %{"token" => token}) do + with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), + %User{} = user <- User.get_cached_by_id(token.user_id) do + render(conn, "reset.html", %{ + token: token, + user: user + }) + else + _e -> render(conn, "invalid_token.html") + end + end + + def do_reset(conn, %{"data" => data}) do + with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do + render(conn, "reset_success.html") + else + _e -> render(conn, "reset_failed.html") + end + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 489170d80..b1863528f 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Activity alias Pleroma.Emoji alias Pleroma.Notification - alias Pleroma.PasswordResetToken - alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub @@ -20,26 +18,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.OStatus alias Pleroma.Web.WebFinger - def show_password_reset(conn, %{"token" => token}) do - with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), - %User{} = user <- User.get_cached_by_id(token.user_id) do - render(conn, "password_reset.html", %{ - token: token, - user: user - }) - else - _e -> render(conn, "invalid_token.html") - end - end - - def password_reset(conn, %{"data" => data}) do - with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do - render(conn, "password_reset_success.html") - else - _e -> render(conn, "password_reset_failed.html") - end - end - def help_test(conn, _params) do json(conn, "ok") end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 6cf107d17..45ef7be3d 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -456,6 +456,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end + def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + change = Changeset.change(user, %{avatar: nil}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + + conn + |> put_view(UserView) + |> render("show.json", %{user: user, for: user}) + end + def update_avatar(%{assigns: %{user: user}} = conn, params) do {:ok, object} = ActivityPub.upload(params, type: :avatar) change = Changeset.change(user, %{avatar: object.data}) @@ -467,6 +477,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do |> render("show.json", %{user: user, for: user}) end + def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + with new_info <- %{"banner" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + response = %{url: nil} |> Jason.encode!() + + conn + |> json_reply(200, response) + end + end + def update_banner(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), new_info <- %{"banner" => object.data}, @@ -482,6 +505,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end + def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + with new_info <- %{"background" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + response = %{url: nil} |> Jason.encode!() + + conn + |> json_reply(200, response) + end + end + def update_background(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(params, type: :background), new_info <- %{"background" => object.data}, diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex new file mode 100644 index 000000000..b166b925d --- /dev/null +++ b/lib/pleroma/web/twitter_api/views/password_view.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordView do + use Pleroma.Web, :view + import Phoenix.HTML.Form +end diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex index 5d8a77346..bf09775e6 100644 --- a/lib/pleroma/web/uploader_controller.ex +++ b/lib/pleroma/web/uploader_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.UploaderController do use Pleroma.Web, :controller @@ -8,7 +12,7 @@ defmodule Pleroma.Web.UploaderController do end def callbacks(conn, _) do - send_resp(conn, 400, "bad request") + render_error(conn, :bad_request, "bad request") end defp process_callback(conn, pid, params) when is_pid(pid) do @@ -20,6 +24,6 @@ defmodule Pleroma.Web.UploaderController do end defp process_callback(conn, _, _) do - send_resp(conn, 400, "bad request") + render_error(conn, :bad_request, "bad request") end end diff --git a/lib/pleroma/web/views/error_view.ex b/lib/pleroma/web/views/error_view.ex index f4c04131c..5cb8669fe 100644 --- a/lib/pleroma/web/views/error_view.ex +++ b/lib/pleroma/web/views/error_view.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ErrorView do def render("500.json", assigns) do Logger.error("Internal server error: #{inspect(assigns[:reason])}") - if Mix.env() != :prod do + if Pleroma.Config.get(:env) != :prod do %{errors: %{detail: "Internal server error", reason: inspect(assigns[:reason])}} else %{errors: %{detail: "Internal server error"}} diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 66813e4dd..b42f6887e 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -23,9 +23,11 @@ defmodule Pleroma.Web do def controller do quote do use Phoenix.Controller, namespace: Pleroma.Web + import Plug.Conn import Pleroma.Web.Gettext import Pleroma.Web.Router.Helpers + import Pleroma.Web.TranslationHelpers plug(:set_put_layout) |