Merge remote-tracking branch 'origin/develop' into feature/local-only-scope

This commit is contained in:
Egor Kislitsyn 2020-10-29 14:51:23 +04:00
commit 8542d2efee
No known key found for this signature in database
GPG key ID: 1B49CB15B71E7805
25 changed files with 396 additions and 259 deletions

View file

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Experimental websocket-based federation between Pleroma instances. - Experimental websocket-based federation between Pleroma instances.
- Support for local-only statuses - Support for local-only statuses
- App metrics: ability to restrict access to specified IP whitelist. - App metrics: ability to restrict access to specified IP whitelist.
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
### Changed ### Changed
@ -51,7 +52,9 @@ switched to a new configuration mechanism, however it was not officially removed
- Add documented-but-missing chat pagination. - Add documented-but-missing chat pagination.
- Allow sending out emails again. - Allow sending out emails again.
- Allow sending chat messages to yourself - Allow sending chat messages to yourself.
- Fix remote users with a whitespace name.
- OStatus / static FE endpoints: fixed inaccessibility for anonymous users on non-federating instances, switched to handling per `:restrict_unauthenticated` setting.
## Unreleased (Patch) ## Unreleased (Patch)

View file

@ -234,6 +234,7 @@
"text/bbcode" "text/bbcode"
], ],
autofollowed_nicknames: [], autofollowed_nicknames: [],
autofollowing_nicknames: [],
max_pinned_statuses: 1, max_pinned_statuses: 1,
attachment_links: false, attachment_links: false,
max_report_comment_size: 1000, max_report_comment_size: 1000,

View file

@ -831,6 +831,12 @@
description: description:
"Set to nicknames of (local) users that every new user should automatically follow" "Set to nicknames of (local) users that every new user should automatically follow"
}, },
%{
key: :autofollowing_nicknames,
type: {:list, :string},
description:
"Set to nicknames of (local) users that automatically follows every newly registered user"
},
%{ %{
key: :attachment_links, key: :attachment_links,
type: :boolean, type: :boolean,

View file

@ -45,6 +45,7 @@ To add configuration to your config file, you can copy it from the base config.
older software for theses nicknames. older software for theses nicknames.
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature. * `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow. * `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
* `autofollowing_nicknames`: Set to nicknames of (local) users that automatically follows every newly registered user.
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses. * `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`). * `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`. * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.

View file

@ -426,7 +426,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
params, params,
[ [
:bio, :bio,
:name,
:emoji, :emoji,
:ap_id, :ap_id,
:inbox, :inbox,
@ -455,7 +454,9 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:accepts_chat_messages :accepts_chat_messages
] ]
) )
|> validate_required([:name, :ap_id]) |> cast(params, [:name], empty_values: [])
|> validate_required([:ap_id])
|> validate_required([:name], trim: false)
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex) |> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
@ -765,6 +766,16 @@ defp autofollow_users(user) do
follow_all(user, autofollowed_users) follow_all(user, autofollowed_users)
end end
defp autofollowing_users(user) do
candidates = Config.get([:instance, :autofollowing_nicknames])
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
|> Enum.each(&follow(&1, user, :follow_accept))
{:ok, :success}
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do with {:ok, user} <- Repo.insert(changeset) do
@ -774,6 +785,7 @@ def register(%Ecto.Changeset{} = changeset) do
def post_register_action(%User{} = user) do def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user), with {:ok, user} <- autofollow_users(user),
{:ok, _} <- autofollowing_users(user),
{:ok, user} <- set_cache(user), {:ok, user} <- set_cache(user),
{:ok, _} <- send_welcome_email(user), {:ok, _} <- send_welcome_email(user),
{:ok, _} <- send_welcome_message(user), {:ok, _} <- send_welcome_message(user),

View file

@ -1378,6 +1378,7 @@ def fetch_and_prepare_user_from_ap_id(ap_id, opts \\ []) do
{:ok, data} <- user_data_from_user_object(data) do {:ok, data} <- user_data_from_user_object(data) do
{:ok, maybe_update_follow_information(data)} {:ok, maybe_update_follow_information(data)}
else else
# If this has been deleted, only log a debug and not an error
{:error, "Object has been deleted" = e} -> {:error, "Object has been deleted" = e} ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}") Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e} {:error, e}

View file

@ -48,29 +48,30 @@ def is_direct?(activity) do
def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false def is_list?(_), do: false
@spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(nil, _), do: false def visible_for_user?(nil, _), do: false
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do def visible_for_user?(
%Activity{data: %{"listMessage" => list_ap_id}} = activity,
%User{} = user
) do
user.ap_id in activity.data["to"] || user.ap_id in activity.data["to"] ||
list_ap_id list_ap_id
|> Pleroma.List.get_by_ap_id() |> Pleroma.List.get_by_ap_id()
|> Pleroma.List.member?(user) |> Pleroma.List.member?(user)
end end
def visible_for_user?(%{local: local} = activity, nil) do def visible_for_user?(%Activity{} = activity, nil) do
cfg_key = if local, do: :local, else: :remote if restrict_unauthenticated_access?(activity),
if Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key),
do: false, do: false,
else: is_public?(activity) else: is_public?(activity)
end end
def visible_for_user?(activity, user) do def visible_for_user?(%Activity{} = activity, user) do
x = [user.ap_id | User.following(user)] x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
is_public?(activity) || Enum.any?(x, &(&1 in y)) is_public?(activity) || Enum.any?(x, &(&1 in y))
@ -86,6 +87,26 @@ def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
result result
end end
def restrict_unauthenticated_access?(%Activity{local: local}) do
restrict_unauthenticated_access_to_activity?(local)
end
def restrict_unauthenticated_access?(%Object{} = object) do
object
|> Object.local?()
|> restrict_unauthenticated_access_to_activity?()
end
def restrict_unauthenticated_access?(%User{} = user) do
User.visible_for(user, _reading_user = nil)
end
defp restrict_unauthenticated_access_to_activity?(local?) when is_boolean(local?) do
cfg_key = if local?, do: :local, else: :remote
Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key)
end
def get_visibility(object) do def get_visibility(object) do
to = object.data["to"] || [] to = object.data["to"] || []
cc = object.data["cc"] || [] cc = object.data["cc"] || []

View file

@ -52,7 +52,7 @@ def render("credentials.json", %{user: user, for: for_user}) do
:skip_thread_containment, :skip_thread_containment,
:pleroma_settings_store, :pleroma_settings_store,
:raw_fields, :raw_fields,
:discoverable, :is_discoverable,
:actor_type :actor_type
]) ])
|> Map.merge(%{ |> Map.merge(%{

View file

@ -10,14 +10,14 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
def feed(conn, params) do def feed(conn, params) do
unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do if Config.get!([:instance, :public]) do
render_feed(conn, params) render_feed(conn, params)
else else
render_error(conn, :not_found, "Not found") render_error(conn, :not_found, "Not found")
end end
end end
def render_feed(conn, %{"tag" => raw_tag} = params) do defp render_feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag) {format, tag} = parse_tag(raw_tag)
activities = activities =
@ -36,12 +36,13 @@ def render_feed(conn, %{"tag" => raw_tag} = params) do
end end
@spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()} @spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()}
defp parse_tag(raw_tag) when is_binary(raw_tag) do defp parse_tag(raw_tag) do
case Enum.reverse(String.split(raw_tag, ".")) do case is_binary(raw_tag) && Enum.reverse(String.split(raw_tag, ".")) do
[format | tag] when format in ["atom", "rss"] -> {format, Enum.join(tag, ".")} [format | tag] when format in ["rss", "atom"] ->
_ -> {"rss", raw_tag} {format, Enum.join(tag, ".")}
end
end
defp parse_tag(raw_tag), do: {"rss", raw_tag} _ ->
{"atom", raw_tag}
end
end
end end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.Feed.UserController do defmodule Pleroma.Web.Feed.UserController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Config
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
@ -22,13 +23,8 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname
def feed_redirect(%{assigns: %{format: format}} = conn, _params) def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do when format in ["json", "activity+json"] do
with %{halted: false} = conn <-
Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn,
unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1
) do
ActivityPubController.call(conn, :user) ActivityPubController.call(conn, :user)
end end
end
def feed_redirect(conn, %{"nickname" => nickname}) do def feed_redirect(conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
@ -36,25 +32,18 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
end end
end end
def feed(conn, params) do def feed(conn, %{"nickname" => nickname} = params) do
unless Pleroma.Config.restrict_unauthenticated_access?(:profiles, :local) do
render_feed(conn, params)
else
errors(conn, {:error, :not_found})
end
end
def render_feed(conn, %{"nickname" => nickname} = params) do
format = get_format(conn) format = get_format(conn)
format = format =
if format in ["rss", "atom"] do if format in ["atom", "rss"] do
format format
else else
"atom" "atom"
end end
with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)},
{_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
activities = activities =
%{ %{
type: ["Create"], type: ["Create"],
@ -69,7 +58,7 @@ def render_feed(conn, %{"nickname" => nickname} = params) do
|> render("user.#{format}", |> render("user.#{format}",
user: user, user: user,
activities: activities, activities: activities,
feed_config: Pleroma.Config.get([:feed]) feed_config: Config.get([:feed])
) )
end end
end end
@ -81,6 +70,8 @@ def errors(conn, {:error, :not_found}) do
def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found}) def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found})
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, {:visibility, _}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong") render_error(conn, :internal_server_error, "Something went wrong")
end end

View file

@ -16,10 +16,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.Router alias Pleroma.Web.Router
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug,
unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1
)
plug( plug(
RateLimiter, RateLimiter,
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
@ -37,14 +33,12 @@ def object(%{assigns: %{format: format}} = conn, _params)
ActivityPubController.call(conn, :object) ActivityPubController.call(conn, :object)
end end
def object(%{assigns: %{format: format}} = conn, _params) do def object(conn, _params) do
with id <- Endpoint.url() <> conn.request_path, with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)}, {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do {_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do redirect(conn, to: "/notice/#{activity.id}")
_ -> redirect(conn, to: "/notice/#{activity.id}")
end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found} {:error, :not_found}
@ -59,13 +53,11 @@ def activity(%{assigns: %{format: format}} = conn, _params)
ActivityPubController.call(conn, :activity) ActivityPubController.call(conn, :activity)
end end
def activity(%{assigns: %{format: format}} = conn, _params) do def activity(conn, _params) do
with id <- Endpoint.url() <> conn.request_path, with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do {_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do redirect(conn, to: "/notice/#{activity.id}")
_ -> redirect(conn, to: "/notice/#{activity.id}")
end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found} {:error, :not_found}
@ -119,6 +111,7 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
def notice_player(conn, %{"id" => id}) do def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%Object{} = object <- Object.normalize(activity), %Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do

View file

@ -34,22 +34,26 @@ def init(opts) do
end end
def call(conn, opts) do def call(conn, opts) do
frontend_type = Map.get(opts, :frontend_type, :primary) with false <- invalid_path?(conn.path_info),
path = file_path("", frontend_type) frontend_type <- Map.get(opts, :frontend_type, :primary),
path when not is_nil(path) <- file_path("", frontend_type) do
if path do call_static(conn, opts, path)
conn
|> call_static(opts, path)
else else
_ ->
conn conn
end end
end end
defp invalid_path?(list) do
invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
end
defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
defp invalid_path?([], _match), do: false
defp call_static(conn, opts, from) do defp call_static(conn, opts, from) do
opts = opts = Map.put(opts, :from, from)
opts
|> Map.put(:from, from)
Plug.Static.call(conn, opts) Plug.Static.call(conn, opts)
end end
end end

View file

@ -5,6 +5,26 @@
defmodule Pleroma.Web.Router do defmodule Pleroma.Web.Router do
use Pleroma.Web, :router use Pleroma.Web, :router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do pipeline :browser do
plug(:accepts, ["html"]) plug(:accepts, ["html"])
plug(:fetch_session) plug(:fetch_session)
@ -566,30 +586,43 @@ defmodule Pleroma.Web.Router do
) )
end end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
pipeline :oembed do
plug(:accepts, ["json", "xml"])
end
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
pipe_through([:ostatus, :http_signature]) # Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
get("/objects/:uuid", OStatus.OStatusController, :object) get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity) get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice) get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
# Mastodon compatibility routes # Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object) get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity) get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
pipe_through([:accepts_html_xml, Pleroma.Web.Plugs.StaticFEPlug])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed) get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end end

View file

@ -17,12 +17,96 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
plug(:assign_id) plug(:assign_id)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug,
unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1
)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"] @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
@doc "Renders requested local public activity or public activities of requested user"
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(notice_id),
true <- Visibility.is_public?(activity.object),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
timeline =
activity.object.data["context"]
|> ActivityPub.fetch_activities_for_context(%{})
|> Enum.reverse()
|> Enum.map(&represent(&1, &1.object.id == activity.object.id))
render(conn, "conversation.html", %{activities: timeline, meta: meta})
else
%Activity{object: %Object{data: data}} ->
conn
|> put_status(:found)
|> redirect(external: data["url"] || data["external_url"] || data["id"])
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
with {_, %User{local: true} = user} <-
{:fetch_user, User.get_cached_by_nickname_or_id(username_or_id)},
{_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
meta = Metadata.build_tags(%{user: user})
params =
params
|> Map.take(@page_keys)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
timeline =
user
|> ActivityPub.fetch_user_activities(_reading_user = nil, params)
|> Enum.map(&represent/1)
prev_page_id =
(params["min_id"] || params["max_id"]) &&
List.first(timeline) && List.first(timeline).id
next_page_id = List.last(timeline) && List.last(timeline).id
render(conn, "profile.html", %{
user: User.sanitize_html(user),
timeline: timeline,
prev_page_id: prev_page_id,
next_page_id: next_page_id,
meta: meta
})
else
_ ->
not_found(conn, "User not found.")
end
end
def show(%{assigns: %{object_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_create_by_object_ap_id_with_object(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{activity_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_by_ap_id(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
do: name do: name
@ -81,91 +165,6 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
} }
end end
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(notice_id),
true <- Visibility.is_public?(activity.object),
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
timeline =
activity.object.data["context"]
|> ActivityPub.fetch_activities_for_context(%{})
|> Enum.reverse()
|> Enum.map(&represent(&1, &1.object.id == activity.object.id))
render(conn, "conversation.html", %{activities: timeline, meta: meta})
else
%Activity{object: %Object{data: data}} ->
conn
|> put_status(:found)
|> redirect(external: data["url"] || data["external_url"] || data["id"])
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
case User.get_cached_by_nickname_or_id(username_or_id) do
%User{} = user ->
meta = Metadata.build_tags(%{user: user})
params =
params
|> Map.take(@page_keys)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
timeline =
user
|> ActivityPub.fetch_user_activities(nil, params)
|> Enum.map(&represent/1)
prev_page_id =
(params["min_id"] || params["max_id"]) &&
List.first(timeline) && List.first(timeline).id
next_page_id = List.last(timeline) && List.last(timeline).id
render(conn, "profile.html", %{
user: User.sanitize_html(user),
timeline: timeline,
prev_page_id: prev_page_id,
next_page_id: next_page_id,
meta: meta
})
_ ->
not_found(conn, "User not found.")
end
end
def show(%{assigns: %{object_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_create_by_object_ap_id_with_object(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{activity_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_by_ap_id(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id) do: assign(conn, :notice_id, notice_id)

46
test/fixtures/mewmew_no_name.json vendored Normal file
View file

@ -0,0 +1,46 @@
{
"@context" : [
"https://www.w3.org/ns/activitystreams",
"https://princess.cat/schemas/litepub-0.1.jsonld",
{
"@language" : "und"
}
],
"attachment" : [],
"capabilities" : {
"acceptsChatMessages" : true
},
"discoverable" : false,
"endpoints" : {
"oauthAuthorizationEndpoint" : "https://princess.cat/oauth/authorize",
"oauthRegistrationEndpoint" : "https://princess.cat/api/v1/apps",
"oauthTokenEndpoint" : "https://princess.cat/oauth/token",
"sharedInbox" : "https://princess.cat/inbox",
"uploadMedia" : "https://princess.cat/api/ap/upload_media"
},
"followers" : "https://princess.cat/users/mewmew/followers",
"following" : "https://princess.cat/users/mewmew/following",
"icon" : {
"type" : "Image",
"url" : "https://princess.cat/media/12794fb50e86911e65be97f69196814049dcb398a2f8b58b99bb6591576e648c.png?name=blobcatpresentpink.png"
},
"id" : "https://princess.cat/users/mewmew",
"image" : {
"type" : "Image",
"url" : "https://princess.cat/media/05d8bf3953ab6028fc920494ffc643fbee9dcef40d7bdd06f107e19acbfbd7f9.png"
},
"inbox" : "https://princess.cat/users/mewmew/inbox",
"manuallyApprovesFollowers" : true,
"name" : " ",
"outbox" : "https://princess.cat/users/mewmew/outbox",
"preferredUsername" : "mewmew",
"publicKey" : {
"id" : "https://princess.cat/users/mewmew#main-key",
"owner" : "https://princess.cat/users/mewmew",
"publicKeyPem" : "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAru7VpygVef4zrFwnj0Mh\nrbO/2z2EdKN3rERtNrT8zWsLXNLQ50lfpRPnGDrd+xq7Rva4EIu0d5KJJ9n4vtY0\nuxK3On9vA2oyjLlR9O0lI3XTrHJborG3P7IPXrmNUMFpHiFHNqHp5tugUrs1gUFq\n7tmOmM92IP4Wjk8qNHFcsfnUbaPTX7sNIhteQKdi5HrTb/6lrEIe4G/FlMKRqxo3\nRNHuv6SNFQuiUKvFzjzazvjkjvBSm+aFROgdHa2tKl88StpLr7xmuY8qNFCRT6W0\nLacRp6c8ah5f03Kd+xCBVhCKvKaF1K0ERnQTBiitUh85md+Mtx/CoDoLnmpnngR3\nvQIDAQAB\n-----END PUBLIC KEY-----\n\n"
},
"summary" : "please reply to my posts as direct messages if you have many followers",
"tag" : [],
"type" : "Person",
"url" : "https://princess.cat/users/mewmew"
}

View file

@ -388,6 +388,7 @@ test "fetches correct profile for nickname beginning with number" do
} }
setup do: clear_config([:instance, :autofollowed_nicknames]) setup do: clear_config([:instance, :autofollowed_nicknames])
setup do: clear_config([:instance, :autofollowing_nicknames])
setup do: clear_config([:welcome]) setup do: clear_config([:welcome])
setup do: clear_config([:instance, :account_activation_required]) setup do: clear_config([:instance, :account_activation_required])
@ -408,6 +409,23 @@ test "it autofollows accounts that are set for it" do
refute User.following?(registered_user, remote_user) refute User.following?(registered_user, remote_user)
end end
test "it adds automatic followers for new registered accounts" do
user1 = insert(:user)
user2 = insert(:user)
Pleroma.Config.put([:instance, :autofollowing_nicknames], [
user1.nickname,
user2.nickname
])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert User.following?(user1, registered_user)
assert User.following?(user2, registered_user)
end
test "it sends a welcome message if it is set" do test "it sends a welcome message if it is set" do
welcome_user = insert(:user) welcome_user = insert(:user)
Pleroma.Config.put([:welcome, :direct_message, :enabled], true) Pleroma.Config.put([:welcome, :direct_message, :enabled], true)

View file

@ -156,21 +156,6 @@ test "it returns error when user is not found", %{conn: conn} do
assert response == "Not found" assert response == "Not found"
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
conn =
put_req_header(
conn,
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user)
end
end end
describe "mastodon compatibility routes" do describe "mastodon compatibility routes" do
@ -338,18 +323,6 @@ test "cached purged after object deletion", %{conn: conn} do
assert "Not found" == json_response(conn2, :not_found) assert "Not found" == json_response(conn2, :not_found)
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user)
end
end end
describe "/activities/:uuid" do describe "/activities/:uuid" do
@ -421,18 +394,6 @@ test "cached purged after activity deletion", %{conn: conn} do
assert "Not found" == json_response(conn2, :not_found) assert "Not found" == json_response(conn2, :not_found)
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user)
end
end end
describe "/inbox" do describe "/inbox" do
@ -893,15 +854,6 @@ test "it returns an announce activity in a collection", %{conn: conn} do
assert response(conn, 200) =~ announce_activity.data["object"] assert response(conn, 200) =~ announce_activity.data["object"]
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user)
end
end end
describe "POST /users/:nickname/outbox (C2S)" do describe "POST /users/:nickname/outbox (C2S)" do

View file

@ -2273,4 +2273,15 @@ test "`following` still contains self-replies by friends" do
assert length(activities) == 2 assert length(activities) == 2
end end
end end
test "allow fetching of accounts with an empty string name field" do
Tesla.Mock.mock(fn
%{method: :get, url: "https://princess.cat/users/mewmew"} ->
file = File.read!("test/fixtures/mewmew_no_name.json")
%Tesla.Env{status: 200, body: file}
end)
{:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew")
assert user.name == " "
end
end end

View file

@ -38,7 +38,6 @@ test "serves app metrics", %{conn: conn} do
for metric <- [ for metric <- [
"http_requests_total", "http_requests_total",
"http_request_duration_microseconds", "http_request_duration_microseconds",
"phoenix_controller_render_duration",
"phoenix_controller_call_duration", "phoenix_controller_call_duration",
"telemetry_scrape_duration", "telemetry_scrape_duration",
"erlang_vm_memory_atom_bytes_total" "erlang_vm_memory_atom_bytes_total"

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
import Pleroma.Factory import Pleroma.Factory
import SweetXml import SweetXml
alias Pleroma.Config
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
@ -15,7 +16,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
setup do: clear_config([:feed]) setup do: clear_config([:feed])
test "gets a feed (ATOM)", %{conn: conn} do test "gets a feed (ATOM)", %{conn: conn} do
Pleroma.Config.put( Config.put(
[:feed, :post_title], [:feed, :post_title],
%{max_length: 25, omission: "..."} %{max_length: 25, omission: "..."}
) )
@ -82,7 +83,7 @@ test "gets a feed (ATOM)", %{conn: conn} do
end end
test "gets a feed (RSS)", %{conn: conn} do test "gets a feed (RSS)", %{conn: conn} do
Pleroma.Config.put( Config.put(
[:feed, :post_title], [:feed, :post_title],
%{max_length: 25, omission: "..."} %{max_length: 25, omission: "..."}
) )
@ -157,7 +158,7 @@ test "gets a feed (RSS)", %{conn: conn} do
response = response =
conn conn
|> put_req_header("accept", "application/rss+xml") |> put_req_header("accept", "application/rss+xml")
|> get(tag_feed_path(conn, :feed, "pleromaart")) |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
|> response(200) |> response(200)
xml = parse(response) xml = parse(response)
@ -183,14 +184,12 @@ test "gets a feed (RSS)", %{conn: conn} do
end end
describe "private instance" do describe "private instance" do
setup do: clear_config([:instance, :public]) setup do: clear_config([:instance, :public], false)
test "returns 404 for tags feed", %{conn: conn} do test "returns 404 for tags feed", %{conn: conn} do
Config.put([:instance, :public], false)
conn conn
|> put_req_header("accept", "application/rss+xml") |> put_req_header("accept", "application/rss+xml")
|> get(tag_feed_path(conn, :feed, "pleromaart")) |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
|> response(404) |> response(404)
end end
end end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
setup do: clear_config([:instance, :federating], true) setup do: clear_config([:static_fe, :enabled], false)
describe "feed" do describe "feed" do
setup do: clear_config([:feed]) setup do: clear_config([:feed])
@ -192,6 +192,16 @@ test "returns 404 when the user is remote", %{conn: conn} do
|> get(user_feed_path(conn, :feed, user.nickname)) |> get(user_feed_path(conn, :feed, user.nickname))
|> response(404) |> response(404)
end end
test "does not require authentication on non-federating instances", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> put_req_header("accept", "application/rss+xml")
|> get("/users/#{user.nickname}/feed.rss")
|> response(200)
end
end end
# Note: see ActivityPubControllerTest for JSON format tests # Note: see ActivityPubControllerTest for JSON format tests

View file

@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Config
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -21,7 +20,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
:ok :ok
end end
setup do: clear_config([:instance, :federating], true) setup do: clear_config([:static_fe, :enabled], false)
describe "Mastodon compatibility routes" do describe "Mastodon compatibility routes" do
setup %{conn: conn} do setup %{conn: conn} do
@ -215,15 +214,16 @@ test "404s a non-existing notice", %{conn: conn} do
assert response(conn, 404) assert response(conn, 404)
end end
test "it requires authentication if instance is NOT federating", %{ test "does not require authentication on non-federating instances", %{
conn: conn conn: conn
} do } do
user = insert(:user) clear_config([:instance, :federating], false)
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
conn = put_req_header(conn, "accept", "text/html") conn
|> put_req_header("accept", "text/html")
ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) |> get("/notice/#{note_activity.id}")
|> response(200)
end end
end end
@ -325,14 +325,16 @@ test "404s when attachment isn't audio or video", %{conn: conn} do
|> response(404) |> response(404)
end end
test "it requires authentication if instance is NOT federating", %{ test "does not require authentication on non-federating instances", %{
conn: conn, conn: conn,
note_activity: note_activity note_activity: note_activity
} do } do
user = insert(:user) clear_config([:instance, :federating], false)
conn = put_req_header(conn, "accept", "text/html")
ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{note_activity.id}/embed_player")
|> response(200)
end end
end end
end end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
import Mock
@dir "test/tmp/instance_static" @dir "test/tmp/instance_static"
@ -53,4 +54,24 @@ test "overrides existing static files for the `pleroma/admin` path", %{conn: con
index = get(conn, "/pleroma/admin/") index = get(conn, "/pleroma/admin/")
assert html_response(index, 200) == "from frontend plug" assert html_response(index, 200) == "from frontend plug"
end end
test "exclude invalid path", %{conn: conn} do
name = "pleroma-fe"
ref = "dist"
clear_config([:media_proxy, :enabled], true)
clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
clear_config([:frontends, :primary], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
File.mkdir_p!("#{path}/proxy/rr/ss")
File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image")
url =
Pleroma.Web.MediaProxy.encode_url("https://pbs.twimg.com/media/Ek7w8WPVcAApOvN.jpg:large")
with_mock Pleroma.ReverseProxy,
call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do
assert %Plug.Conn{status: :success} = get(conn, url)
end
end
end end

View file

@ -6,14 +6,12 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
setup_all do: clear_config([:static_fe, :enabled], true) setup_all do: clear_config([:static_fe, :enabled], true)
setup do: clear_config([:instance, :federating], true)
setup %{conn: conn} do setup %{conn: conn} do
conn = put_req_header(conn, "accept", "text/html") conn = put_req_header(conn, "accept", "text/html")
@ -74,8 +72,27 @@ test "pagination, page 2", %{conn: conn, user: user} do
refute html =~ ">test29<" refute html =~ ">test29<"
end end
test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do test "does not require authentication on non-federating instances", %{
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) conn: conn,
user: user
} do
clear_config([:instance, :federating], false)
conn = get(conn, "/users/#{user.nickname}")
assert html_response(conn, 200) =~ user.nickname
end
test "returns 404 for local user with `restrict_unauthenticated/profiles/local` setting", %{
conn: conn
} do
clear_config([:restrict_unauthenticated, :profiles, :local], true)
local_user = insert(:user, local: true)
conn
|> get("/users/#{local_user.nickname}")
|> html_response(404)
end end
end end
@ -187,10 +204,28 @@ test "302 for remote cached status", %{conn: conn, user: user} do
assert html_response(conn, 302) =~ "redirected" assert html_response(conn, 302) =~ "redirected"
end end
test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do test "does not require authentication on non-federating instances", %{
conn: conn,
user: user
} do
clear_config([:instance, :federating], false)
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) conn = get(conn, "/notice/#{activity.id}")
assert html_response(conn, 200) =~ "testing a thing!"
end
test "returns 404 for local public activity with `restrict_unauthenticated/activities/local` setting",
%{conn: conn, user: user} do
clear_config([:restrict_unauthenticated, :activities, :local], true)
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
conn
|> get("/notice/#{activity.id}")
|> html_response(404)
end end
end end
end end

View file

@ -112,28 +112,6 @@ defp json_response_and_validate_schema(
defp json_response_and_validate_schema(conn, _status) do defp json_response_and_validate_schema(conn, _status) do
flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}") flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}")
end end
defp ensure_federating_or_authenticated(conn, url, user) do
initial_setting = Config.get([:instance, :federating])
on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)
Config.put([:instance, :federating], false)
conn
|> get(url)
|> response(403)
conn
|> assign(:user, user)
|> get(url)
|> response(200)
Config.put([:instance, :federating], true)
conn
|> get(url)
|> response(200)
end
end end
end end