api: ensure only visible posts are interactable
Port of Akkoma PR 1014 with a few changes: - comments regarding akkomafe changed to Pleroma-FE when applicable - different error message for replying to/interacting with invisible post in Pleroma.Web.CommonAPI.ActivityDraft.in_reply_to/1 - split "doesn't do funny things to other users favs" test into three: - can't unfavourite post that isn't favourited - can't unfavourite other user's favs - can't unfavourite other user's favs using their activity - switched order of args for some CommonAPI function since Akkoma hasn't backported our old change for that Pleroma.Web.CommonAPI.ActivityDraft.in_reply_to/1 now refactored to use `with` statement as in Akkoma. Some defp in_reply_to/1 were therefore removed Original PR author: Oneric Original commit message: It doesn't make sense to like, react, reply, etc to something you cannot see and is unexpected for the author of the interacted with post and might make them believe the reacting user actually _can_ see the post. Wrt to fav, reblog, reaction indexes the missing visibility check was also leaking some (presumably/hopefully) low-severity data. Add full-API test for all modes of interactions with private posts.
This commit is contained in:
parent
ed931a668f
commit
59fcb5c96e
11 changed files with 398 additions and 20 deletions
|
|
@ -35,7 +35,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
|
|||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
operationId: "EmojiReactionController.index",
|
||||
responses: %{
|
||||
200 => array_of_reactions_response()
|
||||
200 => array_of_reactions_response(),
|
||||
403 => Operation.response("Access denied", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => status_response(),
|
||||
403 => Operation.response("Access denied", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
|
|
@ -388,6 +389,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
"application/json",
|
||||
AccountOperation.array_of_accounts()
|
||||
),
|
||||
403 => Operation.response("Access denied", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
{:ok, _} = res ->
|
||||
res
|
||||
|
||||
{:error, :not_found} = res ->
|
||||
{:error, reason} = res when reason in [:not_found, :forbidden] ->
|
||||
res
|
||||
|
||||
{:error, e} ->
|
||||
|
|
@ -269,6 +269,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
defp favorite_helper(user, id) do
|
||||
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
|
||||
{_, true} <- {:visible, Visibility.visible_for_user?(object, user)},
|
||||
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
{:common_pipeline,
|
||||
|
|
@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
{:find_object, _} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:visible, _} ->
|
||||
{:error, :forbidden}
|
||||
|
||||
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
|
||||
if {:object, {"already liked by this actor", []}} in changeset.errors do
|
||||
{:ok, :already_liked}
|
||||
|
|
@ -311,11 +315,15 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
{:ok, Activity.t()} | {:error, String.t()}
|
||||
def react_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||
object <- Object.normalize(activity, fetch: false),
|
||||
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:visible, _} ->
|
||||
{:error, dgettext("errors", "Must be able to access post to interact with it")}
|
||||
|
||||
_ ->
|
||||
{:error, dgettext("errors", "Could not add reaction emoji")}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -136,22 +136,29 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do
|
||||
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
|
||||
end
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
|
||||
# If a post was deleted all its activities (except the newly added Delete) are purged too,
|
||||
# thus lookup by Create db ID will yield nil just as if it never existed in the first place.
|
||||
#
|
||||
# We allow replying to Announce here, due to a Pleroma-FE quirk where if presented with an Announce id
|
||||
# it will render it as if it was just the normal referenced post, but use the Announce id for replies
|
||||
# in the in_reply_to_id key of a POST request to /api/v1/statuses, or as an :id in /api/v1/statuses/:id/*.
|
||||
# TODO: Fix this quirk in FE and remove here and other affected places
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
true <- Visibility.visible_for_user?(activity, draft.user),
|
||||
{:type, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
|
||||
%__MODULE__{draft | in_reply_to: activity}
|
||||
else
|
||||
nil ->
|
||||
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: id} = params} = draft) when is_binary(id) do
|
||||
activity = Activity.get_by_id(id)
|
||||
false ->
|
||||
add_error(draft, dgettext("errors", "Replying to a status that is not visibile to user"))
|
||||
|
||||
params =
|
||||
if is_nil(activity) do
|
||||
# Deleted activities are returned as nil
|
||||
Map.put(params, :in_reply_to_status_id, :deleted)
|
||||
else
|
||||
Map.put(params, :in_reply_to_status_id, activity)
|
||||
end
|
||||
|
||||
in_reply_to(%{draft | params: params})
|
||||
{:type, type} ->
|
||||
add_error(draft, dgettext("errors", "Can only reply to posts, not %{type} activities",
|
||||
type: inspect(type)))
|
||||
end
|
||||
end
|
||||
|
||||
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
@doc "DELETE /api/v1/statuses/:id"
|
||||
def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||
# CommonAPI already checks whether user is allowed to delete
|
||||
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
||||
try_render(conn, "show.json",
|
||||
activity: activity,
|
||||
|
|
@ -340,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
_
|
||||
) do
|
||||
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
|
||||
# CommonAPI already checks whether user is allowed to reblog
|
||||
%Activity{} = announce <- Activity.normalize(announce.data) do
|
||||
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
|
||||
end
|
||||
|
|
@ -364,6 +366,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
_
|
||||
) do
|
||||
with {:ok, _fav} <- CommonAPI.favorite(activity_id, user),
|
||||
# CommonAPI already checks whether user is allowed to reblog
|
||||
%Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
|
@ -28,6 +29,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
|
||||
with true <- Pleroma.Config.get([:instance, :show_reactions]),
|
||||
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
||||
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
|
||||
%Object{} = object <- Object.normalize(activity, fetch: false),
|
||||
reactions <- Object.get_emoji_reactions(object) do
|
||||
reactions =
|
||||
|
|
@ -37,6 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
|
||||
render(conn, "index.json", emoji_reactions: reactions, user: user)
|
||||
else
|
||||
{:visible, _} -> {:error, :forbidden}
|
||||
_e -> json(conn, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -76,6 +79,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()
|
||||
|
||||
# CommonAPI checks if allowed to react
|
||||
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
|
||||
|
|
@ -91,6 +95,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()
|
||||
|
||||
# CommonAPI checks only author can revoke reactions
|
||||
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue