summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/pleroma/config.ex2
-rw-r--r--lib/pleroma/application.ex3
-rw-r--r--lib/pleroma/docs/json.ex2
-rw-r--r--lib/pleroma/object/containment.ex2
-rw-r--r--lib/pleroma/plugs/rate_limiter.ex131
-rw-r--r--lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex44
-rw-r--r--lib/pleroma/plugs/rate_limiter/rate_limiter.ex227
-rw-r--r--lib/pleroma/plugs/rate_limiter/supervisor.ex16
-rw-r--r--lib/pleroma/plugs/static_fe_plug.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/auth_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex6
-rw-r--r--lib/pleroma/web/mongooseim/mongoose_im_controller.ex4
-rw-r--r--lib/pleroma/web/nodeinfo/nodeinfo_controller.ex1
-rw-r--r--lib/pleroma/web/oauth/oauth_controller.ex21
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex5
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex2
-rw-r--r--lib/pleroma/web/router.ex1
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex163
-rw-r--r--lib/pleroma/web/static_fe/static_fe_view.ex47
-rw-r--r--lib/pleroma/web/templates/layout/static_fe.html.eex15
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex8
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex37
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex11
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex11
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/error.html.eex7
-rw-r--r--lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex31
28 files changed, 684 insertions, 149 deletions
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 11e4fde43..0e21408b2 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -45,7 +45,7 @@ defmodule Mix.Tasks.Pleroma.Config do
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
config_path = "config/#{env}.exported_from_db.secret.exs"
- {:ok, file} = File.open(config_path, [:write])
+ {:ok, file} = File.open(config_path, [:write, :utf8])
IO.write(file, "use Mix.Config\r\n")
Repo.all(Config)
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index d681eecc8..2b6a55f98 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -36,7 +36,8 @@ defmodule Pleroma.Application do
Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.Daemons.ScheduledActivityDaemon,
- Pleroma.Daemons.ActivityExpirationDaemon
+ Pleroma.Daemons.ActivityExpirationDaemon,
+ Pleroma.Plugs.RateLimiter.Supervisor
] ++
cachex_children() ++
hackney_pool_children() ++
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index 18ba01d58..f2a56d845 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -5,7 +5,7 @@ defmodule Pleroma.Docs.JSON do
def process(descriptions) do
config_path = "docs/generate_config.json"
- with {:ok, file} <- File.open(config_path, [:write]),
+ with {:ok, file} <- File.open(config_path, [:write, :utf8]),
json <- generate_json(descriptions),
:ok <- IO.write(file, json),
:ok <- File.close(file) do
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index a1f9c1250..25aa32f60 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -64,6 +64,8 @@ defmodule Pleroma.Object.Containment do
def contain_origin(id, %{"attributedTo" => actor} = params),
do: contain_origin(id, Map.put(params, "actor", actor))
+ def contain_origin(_id, _data), do: :error
+
def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex
deleted file mode 100644
index 31388f574..000000000
--- a/lib/pleroma/plugs/rate_limiter.ex
+++ /dev/null
@@ -1,131 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Plugs.RateLimiter do
- @moduledoc """
-
- ## Configuration
-
- A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
-
- * The first element: `scale` (Integer). The time scale in milliseconds.
- * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
-
- It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
-
- To disable a limiter set its value to `nil`.
-
- ### Example
-
- config :pleroma, :rate_limit,
- one: {1000, 10},
- two: [{10_000, 10}, {10_000, 50}],
- foobar: nil
-
- Here we have three limiters:
-
- * `one` which is not over 10req/1s
- * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
- * `foobar` which is disabled
-
- ## Usage
-
- AllowedSyntax:
-
- plug(Pleroma.Plugs.RateLimiter, :limiter_name)
- plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
-
- Allowed options:
-
- * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
- * `params` appends values of specified request params (e.g. ["id"]) to bucket name
-
- Inside a controller:
-
- plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
- plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
-
- plug(
- Pleroma.Plugs.RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
- when action in ~w(fav_status unfav_status)a
- )
-
- or inside a router pipeline:
-
- pipeline :api do
- ...
- plug(Pleroma.Plugs.RateLimiter, :one)
- ...
- end
- """
- import Pleroma.Web.TranslationHelpers
- import Plug.Conn
-
- alias Pleroma.User
-
- def init(limiter_name) when is_atom(limiter_name) do
- init({limiter_name, []})
- end
-
- def init({limiter_name, opts}) do
- case Pleroma.Config.get([:rate_limit, limiter_name]) do
- nil -> nil
- config -> {limiter_name, config, opts}
- end
- end
-
- # Do not limit if there is no limiter configuration
- def call(conn, nil), do: conn
-
- def call(conn, settings) do
- case check_rate(conn, settings) do
- {:ok, _count} ->
- conn
-
- {:error, _count} ->
- render_throttled_error(conn)
- end
- end
-
- defp bucket_name(conn, limiter_name, opts) do
- bucket_name = opts[:bucket_name] || limiter_name
-
- if params_names = opts[:params] do
- params_values = for p <- Enum.sort(params_names), do: conn.params[p]
- Enum.join([bucket_name] ++ params_values, ":")
- else
- bucket_name
- end
- end
-
- defp check_rate(
- %{assigns: %{user: %User{id: user_id}}} = conn,
- {limiter_name, [_, {scale, limit}], opts}
- ) do
- bucket_name = bucket_name(conn, limiter_name, opts)
- ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
- end
-
- defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
- bucket_name = bucket_name(conn, limiter_name, opts)
- ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
- end
-
- defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
- check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
- end
-
- def ip(%{remote_ip: remote_ip}) do
- remote_ip
- |> Tuple.to_list()
- |> Enum.join(".")
- end
-
- defp render_throttled_error(conn) do
- conn
- |> render_error(:too_many_requests, "Throttled")
- |> halt()
- end
-end
diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
new file mode 100644
index 000000000..187582ede
--- /dev/null
+++ b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
@@ -0,0 +1,44 @@
+defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
+ use DynamicSupervisor
+
+ import Cachex.Spec
+
+ def start_link(init_arg) do
+ DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
+ end
+
+ def add_limiter(limiter_name, expiration) do
+ {:ok, _pid} =
+ DynamicSupervisor.start_child(
+ __MODULE__,
+ %{
+ id: String.to_atom("rl_#{limiter_name}"),
+ start:
+ {Cachex, :start_link,
+ [
+ limiter_name,
+ [
+ expiration:
+ expiration(
+ default: expiration,
+ interval: check_interval(expiration),
+ lazy: true
+ )
+ ]
+ ]}
+ }
+ )
+ end
+
+ @impl true
+ def init(_init_arg) do
+ DynamicSupervisor.init(strategy: :one_for_one)
+ end
+
+ defp check_interval(exp) do
+ (exp / 2)
+ |> Kernel.trunc()
+ |> Kernel.min(5000)
+ |> Kernel.max(1)
+ end
+end
diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
new file mode 100644
index 000000000..d720508c8
--- /dev/null
+++ b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
@@ -0,0 +1,227 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RateLimiter do
+ @moduledoc """
+
+ ## Configuration
+
+ A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
+
+ * The first element: `scale` (Integer). The time scale in milliseconds.
+ * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
+
+ It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
+
+ To disable a limiter set its value to `nil`.
+
+ ### Example
+
+ config :pleroma, :rate_limit,
+ one: {1000, 10},
+ two: [{10_000, 10}, {10_000, 50}],
+ foobar: nil
+
+ Here we have three limiters:
+
+ * `one` which is not over 10req/1s
+ * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
+ * `foobar` which is disabled
+
+ ## Usage
+
+ AllowedSyntax:
+
+ plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
+ plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
+
+ Allowed options:
+
+ * `name` required, always used to fetch the limit values from the config
+ * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
+ * `params` appends values of specified request params (e.g. ["id"]) to bucket name
+
+ Inside a controller:
+
+ plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
+ plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
+
+ plug(
+ Pleroma.Plugs.RateLimiter,
+ [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+ when action in ~w(fav_status unfav_status)a
+ )
+
+ or inside a router pipeline:
+
+ pipeline :api do
+ ...
+ plug(Pleroma.Plugs.RateLimiter, name: :one)
+ ...
+ end
+ """
+ import Pleroma.Web.TranslationHelpers
+ import Plug.Conn
+
+ alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
+ alias Pleroma.User
+
+ def init(opts) do
+ limiter_name = Keyword.get(opts, :name)
+
+ case Pleroma.Config.get([:rate_limit, limiter_name]) do
+ nil ->
+ nil
+
+ config ->
+ name_root = Keyword.get(opts, :bucket_name, limiter_name)
+
+ %{
+ name: name_root,
+ limits: config,
+ opts: opts
+ }
+ end
+ end
+
+ # Do not limit if there is no limiter configuration
+ def call(conn, nil), do: conn
+
+ def call(conn, settings) do
+ settings
+ |> incorporate_conn_info(conn)
+ |> check_rate()
+ |> case do
+ {:ok, _count} ->
+ conn
+
+ {:error, _count} ->
+ render_throttled_error(conn)
+ end
+ end
+
+ def inspect_bucket(conn, name_root, settings) do
+ settings =
+ settings
+ |> incorporate_conn_info(conn)
+
+ bucket_name = make_bucket_name(%{settings | name: name_root})
+ key_name = make_key_name(settings)
+ limit = get_limits(settings)
+
+ case Cachex.get(bucket_name, key_name) do
+ {:error, :no_cache} ->
+ {:err, :not_found}
+
+ {:ok, nil} ->
+ {0, limit}
+
+ {:ok, value} ->
+ {value, limit - value}
+ end
+ end
+
+ defp check_rate(settings) do
+ bucket_name = make_bucket_name(settings)
+ key_name = make_key_name(settings)
+ limit = get_limits(settings)
+
+ case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
+ {:commit, value} ->
+ {:ok, value}
+
+ {:ignore, value} ->
+ {:error, value}
+
+ {:error, :no_cache} ->
+ initialize_buckets(settings)
+ check_rate(settings)
+ end
+ end
+
+ defp increment_value(nil, _limit), do: {:commit, 1}
+
+ defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
+
+ defp increment_value(val, _limit), do: {:commit, val + 1}
+
+ defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
+ Map.merge(settings, %{
+ mode: :user,
+ conn_params: params,
+ conn_info: "#{user_id}"
+ })
+ end
+
+ defp incorporate_conn_info(settings, %{params: params} = conn) do
+ Map.merge(settings, %{
+ mode: :anon,
+ conn_params: params,
+ conn_info: "#{ip(conn)}"
+ })
+ end
+
+ defp ip(%{remote_ip: remote_ip}) do
+ remote_ip
+ |> Tuple.to_list()
+ |> Enum.join(".")
+ end
+
+ defp render_throttled_error(conn) do
+ conn
+ |> render_error(:too_many_requests, "Throttled")
+ |> halt()
+ end
+
+ defp make_key_name(settings) do
+ ""
+ |> attach_params(settings)
+ |> attach_identity(settings)
+ end
+
+ defp get_scale(_, {scale, _}), do: scale
+
+ defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
+
+ defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
+
+ defp get_limits(%{limits: {_scale, limit}}), do: limit
+
+ defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
+
+ defp get_limits(%{limits: [{_, limit}, _]}), do: limit
+
+ defp make_bucket_name(%{mode: :user, name: name_root}),
+ do: user_bucket_name(name_root)
+
+ defp make_bucket_name(%{mode: :anon, name: name_root}),
+ do: anon_bucket_name(name_root)
+
+ defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
+ param_string =
+ opts
+ |> Keyword.get(:params, [])
+ |> Enum.sort()
+ |> Enum.map(&Map.get(conn_params, &1, ""))
+ |> Enum.join(":")
+
+ "#{input}#{param_string}"
+ end
+
+ defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
+
+ defp initialize_buckets(%{name: name, limits: limits}) do
+ LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
+ LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
+ end
+
+ defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
+ do: "user:#{base}:#{conn_info}"
+
+ defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
+ do: "ip:#{base}:#{conn_info}"
+
+ defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
+ defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
+end
diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex
new file mode 100644
index 000000000..9672f7876
--- /dev/null
+++ b/lib/pleroma/plugs/rate_limiter/supervisor.ex
@@ -0,0 +1,16 @@
+defmodule Pleroma.Plugs.RateLimiter.Supervisor do
+ use Supervisor
+
+ def start_link(opts) do
+ Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
+ end
+
+ def init(_args) do
+ children = [
+ Pleroma.Plugs.RateLimiter.LimiterSupervisor
+ ]
+
+ opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
+ Supervisor.init(children, opts)
+ end
+end
diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex
new file mode 100644
index 000000000..b3fb3c582
--- /dev/null
+++ b/lib/pleroma/plugs/static_fe_plug.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.StaticFEPlug do
+ import Plug.Conn
+ alias Pleroma.Web.StaticFE.StaticFEController
+
+ def init(options), do: options
+
+ def call(conn, _) do
+ if enabled?() and accepts_html?(conn) do
+ conn
+ |> StaticFEController.call(:show)
+ |> halt()
+ else
+ conn
+ end
+ end
+
+ defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
+
+ defp accepts_html?(conn) do
+ conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html")
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 73fad519e..5b01b964b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@relations [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
- plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
- plug(RateLimiter, :relations_actions when action in @relations)
- plug(RateLimiter, :app_account_creation when action == :create)
+ plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
+ plug(RateLimiter, [name: :relations_actions] when action in @relations)
+ plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index bfd5120ba..d9e51de7f 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@local_mastodon_name "Mastodon-Local"
- plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
+ plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
@doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 6cfd68a84..0a929f55b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
- plug(RateLimiter, :search when action in [:search, :search2, :account_search])
+ plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user))
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index e5d016f63..74b223cf4 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
plug(
RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+ [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+ [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
when action in ~w(favourite unfavourite)a
)
- plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+ plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
index 6ed181cff..358600e7d 100644
--- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
+++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex
@@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
alias Pleroma.Repo
alias Pleroma.User
- plug(RateLimiter, :authentication when action in [:user_exists, :check_password])
- plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password)
+ plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
+ plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
def user_exists(conn, %{"user" => username}) do
with %User{} <- Repo.get_by(User, nickname: username, local: true) do
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index d7ae503f6..486b9f6a4 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -46,6 +46,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
data
|> Map.merge(%{quarantined_instances: quarantined})
+ |> Map.put(:enabled, Config.get([:instance, :federating]))
else
%{}
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index fe71aca8c..2aee8cab2 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper
+ alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_session)
plug(:fetch_flash)
- plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization)
+ plug(RateLimiter, [name: :authentication] when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@@ -36,7 +37,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
authorize(conn, Map.merge(params, auth_attrs))
end
- def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
+ def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
else
@@ -44,6 +45,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
+ # Note: the token is set in oauth_plug, but the token and client do not always go together.
+ # For example, MastodonFE's token is set if user requests with another client,
+ # after user already authorized to MastodonFE.
+ # So we have to check client and token.
+ def authorize(
+ %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
+ %{"client_id" => client_id} = params
+ ) do
+ with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
+ ^client_id <- t.app.client_id do
+ handle_existing_authorization(conn, params)
+ else
+ _ -> do_authorize(conn, params)
+ end
+ end
+
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
defp do_authorize(%Plug.Conn{} = conn, params) do
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 6958519de..12a7c2365 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Fallback.RedirectController
alias Pleroma.Activity
alias Pleroma.Object
+ alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView
@@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Router
plug(
- Pleroma.Plugs.RateLimiter,
- {:ap_routes, params: ["uuid"]} when action in [:object, :activity]
+ RateLimiter,
+ [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
)
plug(
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index db6faac83..bc2f1017c 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
when action != :confirmation_resend
)
- plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
+ plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 7349f4cda..93e7e45f9 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -503,6 +503,7 @@ defmodule Pleroma.Web.Router do
pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
+ plug(Pleroma.Plugs.StaticFEPlug)
end
pipeline :oembed do
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
new file mode 100644
index 000000000..8ccf15f4b
--- /dev/null
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -0,0 +1,163 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StaticFE.StaticFEController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.Metadata
+ alias Pleroma.Web.Router.Helpers
+
+ plug(:put_layout, :static_fe)
+ plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
+ plug(:assign_id)
+
+ @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
+
+ defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
+ do: name
+
+ defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
+ do: summary
+
+ defp get_title(_), do: nil
+
+ defp not_found(conn, message) do
+ conn
+ |> put_status(404)
+ |> render("error.html", %{message: message, meta: ""})
+ end
+
+ def get_counts(%Activity{} = activity) do
+ %Object{data: data} = Object.normalize(activity)
+
+ %{
+ likes: data["like_count"] || 0,
+ replies: data["repliesCount"] || 0,
+ announces: data["announcement_count"] || 0
+ }
+ end
+
+ def represent(%Activity{} = activity), do: represent(activity, false)
+
+ def represent(%Activity{object: %Object{data: data}} = activity, selected) do
+ {:ok, user} = User.get_or_fetch(activity.object.data["actor"])
+
+ link =
+ case user.local do
+ true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+ _ -> data["url"] || data["external_url"] || data["id"]
+ end
+
+ %{
+ user: user,
+ title: get_title(activity.object),
+ content: data["content"] || nil,
+ attachment: data["attachment"],
+ link: link,
+ published: data["published"],
+ sensitive: data["sensitive"],
+ selected: selected,
+ counts: get_counts(activity),
+ id: activity.id
+ }
+ 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})
+
+ timeline =
+ ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
+ |> 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,
+ 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
+
+ def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
+ do: assign(conn, :notice_id, notice_id)
+
+ def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
+ do: assign(conn, :username_or_id, user_id)
+
+ def assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
+ do: assign(conn, :object_id, object_id)
+
+ def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
+ do: assign(conn, :activity_id, activity_id)
+
+ def assign_id(conn, _opts), do: conn
+end
diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex
new file mode 100644
index 000000000..821ece9a9
--- /dev/null
+++ b/lib/pleroma/web/static_fe/static_fe_view.ex
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StaticFE.StaticFEView do
+ use Pleroma.Web, :view
+
+ alias Calendar.Strftime
+ alias Pleroma.Emoji.Formatter
+ alias Pleroma.User
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Gettext
+ alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.Metadata.Utils
+ alias Pleroma.Web.Router.Helpers
+
+ use Phoenix.HTML
+
+ @media_types ["image", "audio", "video"]
+
+ def emoji_for_user(%User{} = user) do
+ user.source_data
+ |> Map.get("tag", [])
+ |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
+ |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ {String.trim(name, ":"), url}
+ end)
+ end
+
+ def fetch_media_type(%{"mediaType" => mediaType}) do
+ Utils.fetch_media_type(@media_types, mediaType)
+ end
+
+ def format_date(date) do
+ {:ok, date, _} = DateTime.from_iso8601(date)
+ Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC")
+ end
+
+ def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma")
+
+ def open_content? do
+ Pleroma.Config.get(
+ [:frontend_configurations, :collapse_message_with_subjects],
+ true
+ )
+ end
+end
diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex
new file mode 100644
index 000000000..819632cec
--- /dev/null
+++ b/lib/pleroma/web/templates/layout/static_fe.html.eex
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
+ <title><%= Pleroma.Config.get([:instance, :name]) %></title>
+ <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
+ <link rel="stylesheet" href="/static/static-fe.css">
+ </head>
+ <body>
+ <div class="container">
+ <%= render @view_module, @view_template, assigns %>
+ </div>
+ </body>
+</html>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
new file mode 100644
index 000000000..7e04e9550
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
@@ -0,0 +1,8 @@
+<%= case @mediaType do %>
+<% "audio" -> %>
+<audio src="<%= @url %>" controls="controls"></audio>
+<% "video" -> %>
+<video src="<%= @url %>" controls="controls"></video>
+<% _ -> %>
+<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
+<% end %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
new file mode 100644
index 000000000..df5e5eedd
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
@@ -0,0 +1,37 @@
+<div class="activity" <%= if @selected do %> id="selected" <% end %>>
+ <p class="pull-right">
+ <%= link format_date(@published), to: @link, class: "activity-link" %>
+ </p>
+ <%= render("_user_card.html", %{user: @user}) %>
+ <div class="activity-content">
+ <%= if @title != "" do %>
+ <details <%= if open_content?() do %>open<% end %>>
+ <summary><%= raw @title %></summary>
+ <div class="e-content"><%= raw @content %></div>
+ </details>
+ <% else %>
+ <div class="e-content"><%= raw @content %></div>
+ <% end %>
+ <%= for %{"name" => name, "url" => [url | _]} <- @attachment do %>
+ <%= if @sensitive do %>
+ <details class="nsfw">
+ <summary><%= Gettext.gettext("sensitive media") %></summary>
+ <div>
+ <%= render("_attachment.html", %{name: name, url: url["href"],
+ mediaType: fetch_media_type(url)}) %>
+ </div>
+ </details>
+ <% else %>
+ <%= render("_attachment.html", %{name: name, url: url["href"],
+ mediaType: fetch_media_type(url)}) %>
+ <% end %>
+ <% end %>
+ </div>
+ <%= if @selected do %>
+ <dl class="counts">
+ <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
+ <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
+ <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
+ </dl>
+ <% end %>
+</div>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
new file mode 100644
index 000000000..c7789f9ac
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
@@ -0,0 +1,11 @@
+<div class="p-author h-card">
+ <a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>">
+ <div class="avatar">
+ <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
+ </div>
+ <span class="display-name">
+ <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
+ <span class="nickname"><%= @user.nickname %></span>
+ </span>
+ </a>
+</div>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex
new file mode 100644
index 000000000..2acd84828
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex
@@ -0,0 +1,11 @@
+<header>
+ <h1><%= link instance_name(), to: "/" %></h1>
+</header>
+
+<main>
+ <div class="conversation">
+ <%= for activity <- @activities do %>
+ <%= render("_notice.html", activity) %>
+ <% end %>
+ </div>
+</main>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex
new file mode 100644
index 000000000..d98a1eba7
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex
@@ -0,0 +1,7 @@
+<header>
+ <h1><%= gettext("Oops") %></h1>
+</header>
+
+<main>
+ <p><%= @message %></p>
+</main>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
new file mode 100644
index 000000000..94063c92d
--- /dev/null
+++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
@@ -0,0 +1,31 @@
+<header>
+ <h1><%= link instance_name(), to: "/" %></h1>
+
+ <h3>
+ <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
+ <input type="hidden" name="nickname" value="<%= @user.nickname %>">
+ <input type="hidden" name="profile" value="">
+ <button type="submit" class="collapse">Remote follow</button>
+ </form>
+ <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
+ <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %>
+ </h3>
+ <p><%= raw @user.bio %></p>
+</header>
+
+<main>
+ <div class="activity-stream">
+ <%= for activity <- @timeline do %>
+ <%= render("_notice.html", Map.put(activity, :selected, false)) %>
+ <% end %>
+ <p id="pagination">
+ <%= if @prev_page_id do %>
+ <%= link "«", to: "?min_id=" <> @prev_page_id %>
+ <% end %>
+ <%= if @prev_page_id && @next_page_id, do: " | " %>
+ <%= if @next_page_id do %>
+ <%= link "»", to: "?max_id=" <> @next_page_id %>
+ <% end %>
+ </p>
+ </div>
+</main>