From ec1452fd1cdb9cb1db9b8bad872916d3213489e2 Mon Sep 17 00:00:00 2001 From: href Date: Thu, 14 May 2020 21:36:31 +0200 Subject: Pleroma.MIME: use gen_magic --- lib/pleroma/application.ex | 1 + lib/pleroma/mime.ex | 84 ++++++++++++---------------------------------- 2 files changed, 23 insertions(+), 62 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9d3d92b38..c74255629 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,6 +80,7 @@ defmodule Pleroma.Application do [ Pleroma.Stats, Pleroma.JobQueueMonitor, + Pleroma.MIME, {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex index 6ee055f50..3b406630e 100644 --- a/lib/pleroma/mime.ex +++ b/lib/pleroma/mime.ex @@ -6,8 +6,21 @@ defmodule Pleroma.MIME do @moduledoc """ Returns the mime-type of a binary and optionally a normalized file-name. """ - @default "application/octet-stream" @read_bytes 35 + @pool __MODULE__.GenMagicPool + + def child_spec(_) do + pool_size = Pleroma.Config.get!([:gen_magic_pool, :size]) + name = @pool + + %{ + id: __MODULE__, + start: {GenMagic.Pool, :start_link, [[name: name, pool_size: pool_size]]}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end @spec file_mime_type(String.t(), String.t()) :: {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error @@ -20,9 +33,10 @@ defmodule Pleroma.MIME do @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error def file_mime_type(filename) do - File.open(filename, [:read], fn f -> - check_mime_type(IO.binread(f, @read_bytes)) - end) + case GenMagic.Pool.perform(@pool, filename) do + {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} + error -> error + end end def bin_mime_type(binary, filename) do @@ -34,13 +48,14 @@ defmodule Pleroma.MIME do @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error def bin_mime_type(<>) do - {:ok, check_mime_type(head)} + case GenMagic.Pool.perform(@pool, {:bytes, head}) do + {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} + error -> error + end end def bin_mime_type(_), do: :error - def mime_type(<<_::binary>>), do: {:ok, @default} - defp fix_extension(filename, content_type) do parts = String.split(filename, ".") @@ -62,59 +77,4 @@ defmodule Pleroma.MIME do Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".") end end - - defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do - "image/png" - end - - defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do - "image/gif" - end - - defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do - "image/jpeg" - end - - defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do - "video/webm" - end - - defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do - "video/mp4" - end - - defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do - "audio/mpeg" - end - - defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do - "audio/mpeg" - end - - defp check_mime_type( - <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65, - 0x6F, 0x72, 0x61, _::binary>> - ) do - "video/ogg" - end - - defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do - "audio/ogg" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do - "audio/wav" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do - "image/webp" - end - - defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do - "video/avi" - end - - defp check_mime_type(_) do - @default - end end -- cgit v1.2.3 From f124f6820582d50be83ba7a1709b14ce8ee1abcc Mon Sep 17 00:00:00 2001 From: href Date: Tue, 16 Jun 2020 15:11:45 +0200 Subject: Switch from gen_magic to majic, use Majic.Plug, remove Pleroma.MIME --- lib/pleroma/application.ex | 2 +- lib/pleroma/mime.ex | 80 ---------------------- lib/pleroma/upload.ex | 19 ++--- .../web/activity_pub/activity_pub_controller.ex | 2 + .../mastodon_api/controllers/media_controller.ex | 1 + .../pleroma_api/controllers/account_controller.ex | 5 ++ .../pleroma_api/controllers/mascot_controller.ex | 10 +-- 7 files changed, 25 insertions(+), 94 deletions(-) delete mode 100644 lib/pleroma/mime.ex (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index c74255629..9c74fa00e 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -80,7 +80,7 @@ defmodule Pleroma.Application do [ Pleroma.Stats, Pleroma.JobQueueMonitor, - Pleroma.MIME, + {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]}, {Oban, Config.get(Oban)} ] ++ task_children(@env) ++ diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex deleted file mode 100644 index 3b406630e..000000000 --- a/lib/pleroma/mime.ex +++ /dev/null @@ -1,80 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.MIME do - @moduledoc """ - Returns the mime-type of a binary and optionally a normalized file-name. - """ - @read_bytes 35 - @pool __MODULE__.GenMagicPool - - def child_spec(_) do - pool_size = Pleroma.Config.get!([:gen_magic_pool, :size]) - name = @pool - - %{ - id: __MODULE__, - start: {GenMagic.Pool, :start_link, [[name: name, pool_size: pool_size]]}, - type: :worker, - restart: :permanent, - shutdown: 500 - } - end - - @spec file_mime_type(String.t(), String.t()) :: - {:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error - def file_mime_type(path, filename) do - with {:ok, content_type} <- file_mime_type(path), - filename <- fix_extension(filename, content_type) do - {:ok, content_type, filename} - end - end - - @spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error - def file_mime_type(filename) do - case GenMagic.Pool.perform(@pool, filename) do - {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} - error -> error - end - end - - def bin_mime_type(binary, filename) do - with {:ok, content_type} <- bin_mime_type(binary), - filename <- fix_extension(filename, content_type) do - {:ok, content_type, filename} - end - end - - @spec bin_mime_type(binary()) :: {:ok, String.t()} | :error - def bin_mime_type(<>) do - case GenMagic.Pool.perform(@pool, {:bytes, head}) do - {:ok, %GenMagic.Result{mime_type: content_type}} -> {:ok, content_type} - error -> error - end - end - - def bin_mime_type(_), do: :error - - defp fix_extension(filename, content_type) do - parts = String.split(filename, ".") - - new_filename = - if length(parts) > 1 do - Enum.drop(parts, -1) |> Enum.join(".") - else - Enum.join(parts) - end - - cond do - content_type == "application/octet-stream" -> - filename - - ext = List.first(MIME.extensions(content_type)) -> - new_filename <> "." <> ext - - true -> - Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".") - end - end -end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..a0ba2f4c0 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -57,6 +57,7 @@ defmodule Pleroma.Upload do defstruct [:id, :name, :tempfile, :content_type, :path] @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()} + @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct." def store(upload, opts \\ []) do opts = get_opts(opts) @@ -123,14 +124,13 @@ defmodule Pleroma.Upload do end defp prepare_upload(%Plug.Upload{} = file, opts) do - with :ok <- check_file_size(file.path, opts.size_limit), - {:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do + with :ok <- check_file_size(file.path, opts.size_limit) do {:ok, %__MODULE__{ id: UUID.generate(), - name: name, + name: file.filename, tempfile: file.path, - content_type: content_type + content_type: file.content_type }} end end @@ -138,16 +138,17 @@ defmodule Pleroma.Upload do defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do parsed = Regex.named_captures(~r/(?jpeg|png|gif);base64,(?.*)/, image_data) data = Base.decode64!(parsed["data"], ignore: :whitespace) - hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data))) + hash = Base.encode16(:crypto.hash(:sha256, data), lower: true) with :ok <- check_binary_size(data, opts.size_limit), tmp_path <- tempfile_for_image(data), - {:ok, content_type, name} <- - Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do + {:ok, %{mime_type: content_type}} <- + Majic.perform({:bytes, data}, pool: Pleroma.MajicPool), + [ext | _] <- MIME.extensions(content_type) do {:ok, %__MODULE__{ id: UUID.generate(), - name: name, + name: hash <> "." <> ext, tempfile: tmp_path, content_type: content_type }} @@ -156,7 +157,7 @@ defmodule Pleroma.Upload do # For Mix.Tasks.MigrateLocalUploads defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do - with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do + with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do {:ok, %__MODULE__{upload | content_type: content_type}} end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index f0b5c6e93..e2a5fb9e9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -45,6 +45,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when action in [:read_inbox, :update_outbox, :whoami, :upload_media] ) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media]) + plug( Pleroma.Plugs.Cache, [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 513de279f..06bb718ef 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..97a6ae60d 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -56,6 +56,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + plug( + Majic.Plug, + [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] + ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @doc "POST /api/v1/pleroma/accounts/confirmation_resend" diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index df6c50ca5..4ba4154dd 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation @@ -22,14 +23,15 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do @doc "PUT /api/v1/pleroma/mascot" def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do - with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), - # Reject if not an image - %{type: "image"} = attachment <- render_attachment(object) do + with {:content_type, "image" <> _} <- {:content_type, file.content_type}, + {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do + attachment = render_attachment(object) {:ok, _user} = User.mascot_update(user, attachment) json(conn, attachment) else - %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images") + {:content_type, _} -> + render_error(conn, :unsupported_media_type, "mascots can only be images") end end -- cgit v1.2.3 From 39f7fc5b8ef781c98136d1f9be50a14bff394233 Mon Sep 17 00:00:00 2001 From: href Date: Tue, 16 Jun 2020 19:00:54 +0200 Subject: Update majic & call plug before OpenApiSpex --- lib/pleroma/web/mastodon_api/controllers/media_controller.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/account_controller.ex | 10 +++++----- lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 06bb718ef..09acea7f4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -11,12 +11,12 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 97a6ae60d..c76cbfc48 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -18,6 +18,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do require Pleroma.Constants + plug( + Majic.Plug, + [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] + ) + plug( OpenApiSpex.Plug.PutApiSpec, [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend @@ -56,11 +61,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) - plug( - Majic.Plug, - [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner] - ) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @doc "POST /api/v1/pleroma/accounts/confirmation_resend" diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index 4ba4154dd..7e2f6c328 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -9,10 +9,10 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) - plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation -- cgit v1.2.3 From afa8b469ed0a71247f27efec08d6eeac24b6674f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 Jul 2020 21:12:59 -0500 Subject: Allow restricting public timeline by instance --- lib/pleroma/web/activity_pub/activity_pub.ex | 12 ++---------- lib/pleroma/web/api_spec/operations/timeline_operation.ex | 10 ++++++++++ .../web/mastodon_api/controllers/timeline_controller.ex | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index bc7b5d95a..9ce2b04dd 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -927,16 +927,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_muted_reblogs(query, _), do: query - defp restrict_instance(query, %{instance: instance}) do - users = - from( - u in User, - select: u.ap_id, - where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}") - ) - |> Repo.all() - - from(activity in query, where: activity.actor in ^users) + defp restrict_instance(query, %{instance: instance}) when is_binary(instance) do + from(activity in query, where: ilike(activity.actor, ^"%://#{instance}/%")) end defp restrict_instance(query, _), do: query diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index 8e19bace7..83cdbad69 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -59,6 +59,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do security: [%{"oAuth" => ["read:statuses"]}], parameters: [ local_param(), + instance_param(), only_media_param(), with_muted_param(), exclude_visibilities_param(), @@ -158,6 +159,15 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do ) end + defp instance_param do + Operation.parameter( + :instance, + :query, + %Schema{type: :string}, + "Show only statuses from the given domain" + ) + end + defp with_muted_param do Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users") end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index ab7b1d6aa..7dccc0005 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -110,6 +110,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> Map.put(:blocking_user, user) |> Map.put(:muting_user, user) |> Map.put(:reply_filtering_user, user) + |> Map.put(:instance, params[:instance]) |> ActivityPub.fetch_public_activities() conn -- cgit v1.2.3 From ad9c925efb77287316f5dbac26f6a1b16662910a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 5 Aug 2020 13:08:13 -0500 Subject: Speed up instance timeline query --- lib/pleroma/web/activity_pub/activity_pub.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 9ce2b04dd..76fc9c3ee 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -928,7 +928,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_muted_reblogs(query, _), do: query defp restrict_instance(query, %{instance: instance}) when is_binary(instance) do - from(activity in query, where: ilike(activity.actor, ^"%://#{instance}/%")) + from( + activity in query, + where: fragment("split_part(actor::text, '/'::text, 3) = ?", ^instance) + ) end defp restrict_instance(query, _), do: query -- cgit v1.2.3 From 24ce9c011caf7401fb261c7df4196b2ef9ba3d90 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 5 Aug 2020 19:33:51 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- lib/pleroma/web/api_spec/operations/timeline_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index 83cdbad69..95720df9f 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -169,7 +169,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do end defp with_muted_param do - Operation.parameter(:with_muted, :query, BooleanLike, "Includeactivities by muted users") + Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users") end defp exclude_visibilities_param do -- cgit v1.2.3 From 0d5088c2b83fafd9d8da1f1b04936f831ac5ee87 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 1 Sep 2020 09:37:08 +0300 Subject: remove `unread_conversation_count` from User --- lib/pleroma/conversation.ex | 6 +--- lib/pleroma/conversation/participation.ex | 27 ++++++-------- lib/pleroma/user.ex | 42 ---------------------- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- 4 files changed, 12 insertions(+), 65 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index e76eb0087..77933f0be 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -43,7 +43,7 @@ defmodule Pleroma.Conversation do def maybe_create_recipientships(participation, activity) do participation = Repo.preload(participation, :recipients) - if participation.recipients |> Enum.empty?() do + if Enum.empty?(participation.recipients) do recipients = User.get_all_by_ap_id(activity.recipients) RecipientShip.create(recipients, participation) end @@ -69,10 +69,6 @@ defmodule Pleroma.Conversation do Enum.map(users, fn user -> invisible_conversation = Enum.any?(users, &User.blocks?(user, &1)) - unless invisible_conversation do - User.increment_unread_conversation_count(conversation, user) - end - opts = Keyword.put(opts, :invisible_conversation, invisible_conversation) {:ok, participation} = diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 8bc3e85d6..4c32b273a 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -63,21 +63,10 @@ defmodule Pleroma.Conversation.Participation do end end - def mark_as_read(participation) do - __MODULE__ - |> where(id: ^participation.id) - |> update(set: [read: true]) - |> select([p], p) - |> Repo.update_all([]) - |> case do - {1, [participation]} -> - participation = Repo.preload(participation, :user) - User.set_unread_conversation_count(participation.user) - {:ok, participation} - - error -> - error - end + def mark_as_read(%__MODULE__{} = participation) do + participation + |> change(read: true) + |> Repo.update() end def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do @@ -93,7 +82,6 @@ defmodule Pleroma.Conversation.Participation do |> update([p], set: [read: true]) |> Repo.update_all([]) - {:ok, user} = User.set_unread_conversation_count(user) {:ok, user, []} end @@ -108,7 +96,6 @@ defmodule Pleroma.Conversation.Participation do |> select([p], p) |> Repo.update_all([]) - {:ok, user} = User.set_unread_conversation_count(user) {:ok, user, participations} end @@ -220,6 +207,12 @@ defmodule Pleroma.Conversation.Participation do {:ok, Repo.preload(participation, :recipients, force: true)} end + @spec unread_count(User.t()) :: integer() + def unread_count(%User{id: user_id}) do + from(q in __MODULE__, where: q.user_id == ^user_id and q.read == false) + |> Repo.aggregate(:count, :id) + end + def unread_conversation_count_for_user(user) do from(p in __MODULE__, where: p.user_id == ^user.id, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index d2ad9516f..7fc7a533e 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -129,7 +129,6 @@ defmodule Pleroma.User do field(:hide_followers, :boolean, default: false) field(:hide_follows, :boolean, default: false) field(:hide_favorites, :boolean, default: true) - field(:unread_conversation_count, :integer, default: 0) field(:pinned_activities, {:array, :string}, default: []) field(:email_notifications, :map, default: %{"digest" => false}) field(:mascot, :map, default: nil) @@ -1295,47 +1294,6 @@ defmodule Pleroma.User do |> update_and_set_cache() end - def set_unread_conversation_count(%User{local: true} = user) do - unread_query = Participation.unread_conversation_count_for_user(user) - - User - |> join(:inner, [u], p in subquery(unread_query)) - |> update([u, p], - set: [unread_conversation_count: p.count] - ) - |> where([u], u.id == ^user.id) - |> select([u], u) - |> Repo.update_all([]) - |> case do - {1, [user]} -> set_cache(user) - _ -> {:error, user} - end - end - - def set_unread_conversation_count(user), do: {:ok, user} - - def increment_unread_conversation_count(conversation, %User{local: true} = user) do - unread_query = - Participation.unread_conversation_count_for_user(user) - |> where([p], p.conversation_id == ^conversation.id) - - User - |> join(:inner, [u], p in subquery(unread_query)) - |> update([u, p], - inc: [unread_conversation_count: 1] - ) - |> where([u], u.id == ^user.id) - |> where([u, p], p.count == 0) - |> select([u], u) - |> Repo.update_all([]) - |> case do - {1, [user]} -> set_cache(user) - _ -> {:error, user} - end - end - - def increment_unread_conversation_count(_, user), do: {:ok, user} - @spec get_users_from_set([String.t()], keyword()) :: [User.t()] def get_users_from_set(ap_ids, opts \\ []) do local_only = Keyword.get(opts, :local_only, true) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 864c0417f..1bf53600c 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -386,7 +386,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do data |> Kernel.put_in( [:pleroma, :unread_conversation_count], - user.unread_conversation_count + Pleroma.Conversation.Participation.unread_count(user) ) end -- cgit v1.2.3 From 23ca5f75afa7369ff52772c39dc3324e9402b230 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:39:08 -0500 Subject: Make it possible to bulk send confirmation emails to all unconfirmed users --- lib/mix/tasks/pleroma/email.ex | 19 ++++++++++++++++++- lib/pleroma/user/query.ex | 4 ++++ 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index d3fac6ec8..61d431971 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Pleroma.Email do use Mix.Task import Mix.Pleroma - @shortdoc "Simple Email test" + @shortdoc "Email administrative tasks" @moduledoc File.read!("docs/administration/CLI_tasks/email.md") def run(["test" | args]) do @@ -21,4 +21,21 @@ defmodule Mix.Tasks.Pleroma.Email do shell_info("Test email has been sent to #{inspect(email.to)} from #{inspect(email.from)}") end + + def run(["resend_confirmation_emails"]) do + start_pleroma() + + Pleroma.User.Query.build(%{ + local: true, + deactivated: false, + confirmation_pending: true, + invisible: false + }) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> Pleroma.User.send_confirmation_email(user) end) + end) + |> Stream.run() + end end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index d618432ff..f59ca6f9c 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -148,6 +148,10 @@ defmodule Pleroma.User.Query do |> where([u], not is_nil(u.nickname)) end + defp compose_query({:confirmation_pending, bool}, query) do + where(query, [u], u.confirmation_pending == ^bool) + end + defp compose_query({:need_approval, _}, query) do where(query, [u], u.approval_pending) end -- cgit v1.2.3 From 75b6fef25dd81c81cd5709739f97182045eec5b8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:39:41 -0500 Subject: Add mix task for bulk [un]confirming the local instance users --- lib/mix/tasks/pleroma/user.ex | 66 +++++++++++++++++++++++++++++++++++++++---- lib/pleroma/user.ex | 7 +++++ lib/pleroma/user/query.ex | 8 +++--- 3 files changed, 71 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 01824aa18..4073fe5a4 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -196,17 +196,24 @@ defmodule Mix.Tasks.Pleroma.User do OptionParser.parse( rest, strict: [ - moderator: :boolean, admin: :boolean, - locked: :boolean + confirmed: :boolean, + locked: :boolean, + moderator: :boolean ] ) with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do user = - case Keyword.get(options, :moderator) do + case Keyword.get(options, :admin) do nil -> user - value -> set_moderator(user, value) + value -> set_admin(user, value) + end + + user = + case Keyword.get(options, :confirmed) do + nil -> user + value -> set_confirmed(user, value) end user = @@ -216,9 +223,9 @@ defmodule Mix.Tasks.Pleroma.User do end _user = - case Keyword.get(options, :admin) do + case Keyword.get(options, :moderator) do nil -> user - value -> set_admin(user, value) + value -> set_moderator(user, value) end else _ -> @@ -353,6 +360,42 @@ defmodule Mix.Tasks.Pleroma.User do end end + def run(["confirm_all"]) do + start_pleroma() + + Pleroma.User.Query.build(%{ + local: true, + deactivated: false, + is_moderator: false, + is_admin: false, + invisible: false + }) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> User.need_confirmation(user, false) end) + end) + |> Stream.run() + end + + def run(["unconfirm_all"]) do + start_pleroma() + + Pleroma.User.Query.build(%{ + local: true, + deactivated: false, + is_moderator: false, + is_admin: false, + invisible: false + }) + |> Pleroma.RepoStreamer.chunk_stream(500) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> User.need_confirmation(user, true) end) + end) + |> Stream.run() + end + def run(["sign_out", nickname]) do start_pleroma() @@ -410,4 +453,15 @@ defmodule Mix.Tasks.Pleroma.User do shell_info("Locked status of #{user.nickname}: #{user.locked}") user end + + defp set_confirmed(user, value) do + {:ok, user} = + case value do + true -> User.need_confirmation(user, false) + false -> User.need_confirmation(user, true) + end + + shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}") + user + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f323fc6ed..603fc3b44 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2123,6 +2123,13 @@ defmodule Pleroma.User do Enum.map(users, &toggle_confirmation/1) end + @spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()} + def need_confirmation(%User{} = user, bool) do + user + |> confirmation_changeset(need_confirmation: bool) + |> update_and_set_cache() + end + def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do mascot end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index f59ca6f9c..64bb24c0e 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -107,12 +107,12 @@ defmodule Pleroma.User.Query do where(query, [u], fragment("? && ?", u.tags, ^tags)) end - defp compose_query({:is_admin, _}, query) do - where(query, [u], u.is_admin) + defp compose_query({:is_admin, bool}, query) do + where(query, [u], u.is_admin == ^bool) end - defp compose_query({:is_moderator, _}, query) do - where(query, [u], u.is_moderator) + defp compose_query({:is_moderator, bool}, query) do + where(query, [u], u.is_moderator == ^bool) end defp compose_query({:super_users, _}, query) do -- cgit v1.2.3 From d23d0c27c25e3ce7b39cae6e504062b4cb389ea4 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 8 Sep 2020 16:48:54 -0500 Subject: Handle possibility of user account in a bulk operation not having an email address --- lib/mix/tasks/pleroma/email.ex | 2 +- lib/pleroma/user.ex | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 61d431971..c0bef0386 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -34,7 +34,7 @@ defmodule Mix.Tasks.Pleroma.Email do |> Pleroma.RepoStreamer.chunk_stream(500) |> Stream.each(fn users -> users - |> Enum.each(fn user -> Pleroma.User.send_confirmation_email(user) end) + |> Enum.each(fn user -> Pleroma.User.try_send_confirmation_email(user) end) end) |> Stream.run() end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 603fc3b44..9dea39619 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -814,7 +814,8 @@ defmodule Pleroma.User do def send_welcome_email(_), do: {:ok, :noop} @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop} - def try_send_confirmation_email(%User{confirmation_pending: true} = user) do + def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user) + when is_binary(email) do if Config.get([:instance, :account_activation_required]) do send_confirmation_email(user) {:ok, :enqueued} -- cgit v1.2.3 From 9853c90abba213bdc87dccf5620cb0d9eb19c049 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Thu, 10 Sep 2020 12:24:44 +0300 Subject: added paginate links to headers for /chats/:id/messages --- lib/pleroma/chat.ex | 24 +++++++-- .../web/api_spec/operations/chat_operation.ex | 3 +- lib/pleroma/web/controller_helper.ex | 20 +++---- .../web/pleroma_api/controllers/chat_controller.ex | 62 +++++++++------------- 4 files changed, 55 insertions(+), 54 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex index 24a86371e..202fffb8a 100644 --- a/lib/pleroma/chat.ex +++ b/lib/pleroma/chat.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Chat do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Pleroma.Repo alias Pleroma.User @@ -16,6 +17,7 @@ defmodule Pleroma.Chat do It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. """ + @type t :: %__MODULE__{} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} schema "chats" do @@ -39,16 +41,28 @@ defmodule Pleroma.Chat do |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) end + @spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) :: + {:ok, t()} | {:error, :not_found} + def get_by_user_and_id(%User{id: user_id}, id) do + from(c in __MODULE__, + where: c.id == ^id, + where: c.user_id == ^user_id + ) + |> Repo.find_resource() + end + + @spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil def get_by_id(id) do - __MODULE__ - |> Repo.get(id) + Repo.get(__MODULE__, id) end + @spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil def get(user_id, recipient) do - __MODULE__ - |> Repo.get_by(user_id: user_id, recipient: recipient) + Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient) end + @spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) :: + {:ok, t()} | {:error, Ecto.Changeset.t()} def get_or_create(user_id, recipient) do %__MODULE__{} |> changeset(%{user_id: user_id, recipient: recipient}) @@ -60,6 +74,8 @@ defmodule Pleroma.Chat do ) end + @spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) :: + {:ok, t()} | {:error, Ecto.Changeset.t()} def bump_or_create(user_id, recipient) do %__MODULE__{} |> changeset(%{user_id: user_id, recipient: recipient}) diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index b1a0d26ab..8cbea9ec4 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -158,7 +158,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do "The messages in the chat", "application/json", chat_messages_response() - ) + ), + 404 => Operation.response("Not Found", "application/json", ApiError) }, security: [ %{ diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 6445966e0..69188a882 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -48,13 +48,13 @@ defmodule Pleroma.Web.ControllerHelper do defp param_to_integer(_, default), do: default - def add_link_headers(conn, activities, extra_params \\ %{}) + def add_link_headers(conn, entries, extra_params \\ %{}) - def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params), + def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _entries, _extra_params), do: conn - def add_link_headers(conn, activities, extra_params) do - case get_pagination_fields(conn, activities, extra_params) do + def add_link_headers(conn, entries, extra_params) do + case get_pagination_fields(conn, entries, extra_params) do %{"next" => next_url, "prev" => prev_url} -> put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") @@ -78,19 +78,15 @@ defmodule Pleroma.Web.ControllerHelper do } end - def get_pagination_fields(conn, activities, extra_params \\ %{}) do - case List.last(activities) do + def get_pagination_fields(conn, entries, extra_params \\ %{}) do + case List.last(entries) do %{pagination_id: max_id} when not is_nil(max_id) -> - %{pagination_id: min_id} = - activities - |> List.first() + %{pagination_id: min_id} = List.first(entries) build_pagination_fields(conn, min_id, max_id, extra_params) %{id: max_id} -> - %{id: min_id} = - activities - |> List.first() + %{id: min_id} = List.first(entries) build_pagination_fields(conn, min_id, max_id, extra_params) diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index e8a1746d4..7b5f3daf9 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do use Pleroma.Web, :controller + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + alias Pleroma.Activity alias Pleroma.Chat alias Pleroma.Chat.MessageReference @@ -47,7 +49,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do }) do with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id), - ^chat_id <- cm_ref.chat_id |> to_string(), + ^chat_id <- to_string(cm_ref.chat_id), %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), {:ok, _} <- remove_or_delete(cm_ref, user) do conn @@ -68,18 +70,13 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do end end - defp remove_or_delete(cm_ref, _) do - cm_ref - |> MessageReference.delete() - end + defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref) def post_chat_message( - %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn, - %{ - id: id - } + %{body_params: params, assigns: %{user: user}} = conn, + %{id: id} ) do - with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), + with {:ok, chat} <- Chat.get_by_user_and_id(user, id), %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, params[:content], @@ -93,13 +90,12 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do end end - def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{ - id: chat_id, - message_id: message_id - }) do - with %MessageReference{} = cm_ref <- - MessageReference.get_by_id(message_id), - ^chat_id <- cm_ref.chat_id |> to_string(), + def mark_message_as_read( + %{assigns: %{user: %{id: user_id}}} = conn, + %{id: chat_id, message_id: message_id} + ) do + with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id), + ^chat_id <- to_string(cm_ref.chat_id), %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do conn @@ -109,36 +105,28 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do end def mark_as_read( - %{ - body_params: %{last_read_id: last_read_id}, - assigns: %{user: %{id: user_id}} - } = conn, + %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn, %{id: id} ) do - with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), - {_n, _} <- - MessageReference.set_all_seen_for_chat(chat, last_read_id) do + with {:ok, chat} <- Chat.get_by_user_and_id(user, id), + {_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do conn |> put_view(ChatView) |> render("show.json", chat: chat) end end - def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do - with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do - cm_refs = + def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do + with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do + chat_message_refs = chat |> MessageReference.for_chat_query() |> Pagination.fetch_paginated(params) conn + |> add_link_headers(chat_message_refs) |> put_view(MessageReferenceView) - |> render("index.json", chat_message_references: cm_refs) - else - _ -> - conn - |> put_status(:not_found) - |> json(%{error: "not found"}) + |> render("index.json", chat_message_references: chat_message_refs) end end @@ -158,8 +146,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do |> render("index.json", chats: chats) end - def create(%{assigns: %{user: user}} = conn, params) do - with %User{ap_id: recipient} <- User.get_by_id(params[:id]), + def create(%{assigns: %{user: user}} = conn, %{id: id}) do + with %User{ap_id: recipient} <- User.get_cached_by_id(id), {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do conn |> put_view(ChatView) @@ -167,8 +155,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do end end - def show(%{assigns: %{user: user}} = conn, params) do - with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do + def show(%{assigns: %{user: user}} = conn, %{id: id}) do + with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do conn |> put_view(ChatView) |> render("show.json", chat: chat) -- cgit v1.2.3 From e5927e92a641a799f9b4e89466374be3c76c4ef2 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 21 Sep 2020 17:08:49 -0500 Subject: Fix deprecation warning for welcome message --- lib/pleroma/config/deprecation_warnings.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 98c4dc9c8..51ddb2889 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -83,9 +83,9 @@ defmodule Pleroma.Config.DeprecationWarnings do if use_old_config do Logger.error(""" !!!DEPRECATION WARNING!!! - Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace: - \n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname` - \n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message` + Your config is using the old namespace for Welcome messages configuration. You need to convert to the new namespace. e.g., + \n* `config :pleroma, :instance, welcome_user_nickname` and `config :pleroma, :instance, welcome_message` are now equal to: + \n* `config :pleroma, :welcome, direct_message: [enabled: true, sender_nickname: "NICKNAME", message: "Your welcome message"]`" """) :error -- cgit v1.2.3 From ee3052a2d8fda37e27f31c8d824ce7ac174b993c Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 22 Sep 2020 14:20:19 +0200 Subject: ActivityPub: Return Announces when filtering by `following`. --- lib/pleroma/web/activity_pub/activity_pub.ex | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index aacd58d03..eb44cffec 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -790,7 +790,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do [activity, object] in query, where: fragment( - "?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?", + """ + ?->>'type' != 'Create' -- This isn't a Create + OR ?->>'inReplyTo' is null -- this isn't a reply + OR ? && array_remove(?, ?) -- The recipient is us or one of our friends, + -- unless they are the author (because authors + -- are also part of the recipients). This leads + -- to a bug that self-replies by friends won't + -- show up. + OR ? = ? -- The actor is us + """, + activity.data, object.data, ^[user.ap_id | User.get_cached_user_friends_ap_ids(user)], activity.recipients, -- cgit v1.2.3 From 0e0ece251af9f85d80968df5b43fe49b85f35434 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 22 Sep 2020 16:55:40 +0400 Subject: Filter out internal users by default --- lib/pleroma/user/query.ex | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index d618432ff..193b90d9d 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -47,6 +47,7 @@ defmodule Pleroma.User.Query do is_moderator: boolean(), super_users: boolean(), invisible: boolean(), + internal: boolean(), followers: User.t(), friends: User.t(), recipients_from_activity: [String.t()], @@ -80,7 +81,9 @@ defmodule Pleroma.User.Query do end defp prepare_query(query, criteria) do - Enum.reduce(criteria, query, &compose_query/2) + criteria + |> Map.put_new(:internal, false) + |> Enum.reduce(query, &compose_query/2) end defp compose_query({key, value}, query) @@ -129,14 +132,12 @@ defmodule Pleroma.User.Query do defp compose_query({:active, _}, query) do User.restrict_deactivated(query) - |> where([u], not is_nil(u.nickname)) |> where([u], u.approval_pending == false) end defp compose_query({:legacy_active, _}, query) do query |> where([u], fragment("not (?->'deactivated' @> 'true')", u.info)) - |> where([u], not is_nil(u.nickname)) end defp compose_query({:deactivated, false}, query) do @@ -145,7 +146,6 @@ defmodule Pleroma.User.Query do defp compose_query({:deactivated, true}, query) do where(query, [u], u.deactivated == ^true) - |> where([u], not is_nil(u.nickname)) end defp compose_query({:need_approval, _}, query) do @@ -199,10 +199,15 @@ defmodule Pleroma.User.Query do limit(query, ^limit) end + defp compose_query({:internal, false}, query) do + query + |> where([u], not is_nil(u.nickname)) + |> where([u], not like(u.nickname, "internal.%")) + end + defp compose_query(_unsupported_param, query), do: query defp location_query(query, local) do where(query, [u], u.local == ^local) - |> where([u], not is_nil(u.nickname)) end end -- cgit v1.2.3 From 2fc3b46ee7e8ee8c22d4cf0039bbe9755a0e3beb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:13:36 -0500 Subject: Remove deprecation warning. We were supposed to remove backwards compat for this in 2.1. --- lib/pleroma/config/deprecation_warnings.ex | 26 -------------------------- 1 file changed, 26 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 51ddb2889..091d02366 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -33,34 +33,8 @@ defmodule Pleroma.Config.DeprecationWarnings do end end - def mrf_user_allowlist do - config = Config.get(:mrf_user_allowlist) - - if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do - rewritten = - Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc -> - Map.put(acc, to_string(k), v) - end) - - Config.put(:mrf_user_allowlist, rewritten) - - Logger.error(""" - !!!DEPRECATION WARNING!!! - As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format. - Pleroma 2.1 will remove support for the old format. Please change your configuration to match this: - - config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)} - """) - - :error - else - :ok - end - end - def warn do with :ok <- check_hellthread_threshold(), - :ok <- mrf_user_allowlist(), :ok <- check_old_mrf_config(), :ok <- check_media_proxy_whitelist_config(), :ok <- check_welcome_message_config(), -- cgit v1.2.3 From e2dcf039d24b1606c90cea75ef11c79b7677c209 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 22 Sep 2020 11:15:40 -0500 Subject: Fix gun_pool_options deprecation warning message --- lib/pleroma/config/deprecation_warnings.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 091d02366..4ba6eaa77 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -122,7 +122,7 @@ defmodule Pleroma.Config.DeprecationWarnings do if timeout = pool_config[:await_up_timeout] do Logger.warn(""" !!!DEPRECATION WARNING!!! - Your config is using old setting name `await_up_timeout` instead of `connect_timeout`. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. + Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`. Please change to `config :pleroma, :connections_pool, connect_timeout` to ensure compatibility with future releases. """) Config.put(:connections_pool, Keyword.put_new(pool_config, :connect_timeout, timeout)) -- cgit v1.2.3 From 5e86a2809e37100b54e0fc88db79245e13f684aa Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 23 Sep 2020 11:45:32 +0200 Subject: transmogrifier: Drop incoming create early if it already exists --- lib/pleroma/web/activity_pub/transmogrifier.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index aa6a69463..d7dd9fe6b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -515,15 +515,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype}} = data, + %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, _options ) when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do data = Map.put(data, "object", strip_internal_fields(data["object"])) with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), + nil <- Activity.get_create_by_object_ap_id(obj_id), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} + else + %Activity{} = activity -> {:ok, activity} + e -> e end end -- cgit v1.2.3 From 8af8eb5ce79a35e01415763164533c537e017776 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 23 Sep 2020 12:32:47 -0500 Subject: Chase Pleroma.RepoStreamer.chunk_stream -> Pleroma.Repo.chunk_stream --- lib/mix/tasks/pleroma/email.ex | 7 ++----- lib/mix/tasks/pleroma/user.ex | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index c0bef0386..1f543241a 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -31,11 +31,8 @@ defmodule Mix.Tasks.Pleroma.Email do confirmation_pending: true, invisible: false }) - |> Pleroma.RepoStreamer.chunk_stream(500) - |> Stream.each(fn users -> - users - |> Enum.each(fn user -> Pleroma.User.try_send_confirmation_email(user) end) - end) + |> Pleroma.Repo.chunk_stream(500, :batches) + |> Stream.each(&Pleroma.User.try_send_confirmation_email(&1)) |> Stream.run() end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 8196e34b1..d50205600 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -370,7 +370,7 @@ defmodule Mix.Tasks.Pleroma.User do is_admin: false, invisible: false }) - |> Pleroma.RepoStreamer.chunk_stream(500) + |> Pleroma.Repo.chunk_stream(500, :batches) |> Stream.each(fn users -> users |> Enum.each(fn user -> User.need_confirmation(user, false) end) @@ -388,7 +388,7 @@ defmodule Mix.Tasks.Pleroma.User do is_admin: false, invisible: false }) - |> Pleroma.RepoStreamer.chunk_stream(500) + |> Pleroma.Repo.chunk_stream(500, :batches) |> Stream.each(fn users -> users |> Enum.each(fn user -> User.need_confirmation(user, true) end) -- cgit v1.2.3 From 9b6d89ff8c798079f4db18eb2b5c66a7426ecbc5 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 27 Jun 2020 13:43:25 +0300 Subject: support for special chars in pack name --- lib/pleroma/emoji/pack.ex | 5 +++-- .../api_spec/operations/pleroma_emoji_pack_operation.ex | 2 +- lib/pleroma/web/router.ex | 16 ++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 0b3f8f00b..9901aa832 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -244,7 +244,8 @@ defmodule Pleroma.Emoji.Pack do uri = url |> String.trim() |> URI.parse() with :ok <- validate_shareable_packs_available(uri), - {:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(), + {:ok, remote_pack} <- + uri |> URI.merge("/api/pleroma/emoji/packs/show?name=#{name}") |> http_get(), {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), {:ok, archive} <- download_archive(url, sha), pack <- copy_as(remote_pack, as || name), @@ -572,7 +573,7 @@ defmodule Pleroma.Emoji.Pack do {:ok, %{ sha: sha, - url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() + url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string() }} %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 59548af13..87ee5feb4 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -192,7 +192,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do end defp name_param do - Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true) end defp url_param do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 42a9db21d..6f591b12f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -234,21 +234,21 @@ defmodule Pleroma.Web.Router do get("/remote", EmojiPackController, :remote) post("/download", EmojiPackController, :download) - post("/:name", EmojiPackController, :create) - patch("/:name", EmojiPackController, :update) - delete("/:name", EmojiPackController, :delete) + post("/create", EmojiPackController, :create) + patch("/update", EmojiPackController, :update) + delete("/delete", EmojiPackController, :delete) - post("/:name/files", EmojiFileController, :create) - patch("/:name/files", EmojiFileController, :update) - delete("/:name/files", EmojiFileController, :delete) + post("/files", EmojiFileController, :add_file) + patch("/files", EmojiFileController, :update_file) + delete("/files", EmojiFileController, :delete_file) end # Pack info / downloading scope "/packs" do pipe_through(:api) get("/", EmojiPackController, :index) - get("/:name", EmojiPackController, :show) - get("/:name/archive", EmojiPackController, :archive) + get("/show", EmojiPackController, :show) + get("/archive", EmojiPackController, :archive) end end -- cgit v1.2.3 From dbbc8016670166c24a29dcc3e2f0d66bb2f4e35f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 27 Jun 2020 14:33:49 +0300 Subject: pagination for remote emoji packs --- lib/pleroma/emoji/pack.ex | 8 ++++---- .../api_spec/operations/pleroma_emoji_pack_operation.ex | 16 +++++++++++++++- .../web/pleroma_api/controllers/emoji_pack_controller.ex | 5 +++-- 3 files changed, 22 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 9901aa832..4420eff5a 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -198,13 +198,13 @@ defmodule Pleroma.Emoji.Pack do end end - @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()} - def list_remote(url) do - uri = url |> String.trim() |> URI.parse() + @spec list_remote(keyword()) :: {:ok, map()} | {:error, atom()} + def list_remote(opts) do + uri = opts[:url] |> String.trim() |> URI.parse() with :ok <- validate_shareable_packs_available(uri) do uri - |> URI.merge("/api/pleroma/emoji/packs") + |> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}") |> http_get() end end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex index 87ee5feb4..79f52dcb3 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -19,7 +19,21 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do tags: ["Emoji Packs"], summary: "Make request to another instance for emoji packs list", security: [%{"oAuth" => ["write"]}], - parameters: [url_param()], + parameters: [ + url_param(), + Operation.parameter( + :page, + :query, + %Schema{type: :integer, default: 1}, + "Page" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 30}, + "Number of emoji to return" + ) + ], operationId: "PleromaAPI.EmojiPackController.remote", responses: %{ 200 => emoji_packs_response(), diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index e3969fee1..6696f8b92 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -23,8 +23,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation - def remote(conn, %{url: url}) do - with {:ok, packs} <- Pack.list_remote(url) do + def remote(conn, params) do + with {:ok, packs} <- + Pack.list_remote(url: params.url, page_size: params.page_size, page: params.page) do json(conn, packs) else {:error, :not_shareable} -> -- cgit v1.2.3 From 8c6ec4c111081b34f68363ce20423e2f338fa2dd Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sun, 20 Sep 2020 09:51:36 +0300 Subject: pack routes change --- lib/pleroma/emoji/pack.ex | 4 ++-- lib/pleroma/web/router.ex | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 4420eff5a..8f1989ada 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -245,7 +245,7 @@ defmodule Pleroma.Emoji.Pack do with :ok <- validate_shareable_packs_available(uri), {:ok, remote_pack} <- - uri |> URI.merge("/api/pleroma/emoji/packs/show?name=#{name}") |> http_get(), + uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(), {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), {:ok, archive} <- download_archive(url, sha), pack <- copy_as(remote_pack, as || name), @@ -524,7 +524,7 @@ defmodule Pleroma.Emoji.Pack do defp http_get(%URI{} = url), do: url |> to_string() |> http_get() defp http_get(url) do - with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do + with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], pool: :default) do Jason.decode(body) end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6f591b12f..707d5e1c4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -226,6 +226,20 @@ defmodule Pleroma.Web.Router do end scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do + scope "/pack" do + pipe_through(:admin_api) + + post("/", EmojiPackController, :create) + patch("/", EmojiPackController, :update) + delete("/", EmojiPackController, :delete) + end + + scope "/pack" do + pipe_through(:api) + + get("/", EmojiPackController, :show) + end + # Modifying packs scope "/packs" do pipe_through(:admin_api) @@ -246,8 +260,8 @@ defmodule Pleroma.Web.Router do # Pack info / downloading scope "/packs" do pipe_through(:api) + get("/", EmojiPackController, :index) - get("/show", EmojiPackController, :show) get("/archive", EmojiPackController, :archive) end end -- cgit v1.2.3 From 5d7ec00bedc61e8899941c374604ae5854c62f4c Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 24 Sep 2020 09:42:30 +0300 Subject: fixes after rebase --- .../web/api_spec/operations/pleroma_emoji_file_operation.ex | 2 +- lib/pleroma/web/router.ex | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex index efbfce75f..a56641426 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_file_operation.ex @@ -126,7 +126,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do end defp name_param do - Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + Operation.parameter(:name, :query, :string, "Pack Name", example: "cofe", required: true) end defp files_object do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 707d5e1c4..e22b31b4c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -248,13 +248,9 @@ defmodule Pleroma.Web.Router do get("/remote", EmojiPackController, :remote) post("/download", EmojiPackController, :download) - post("/create", EmojiPackController, :create) - patch("/update", EmojiPackController, :update) - delete("/delete", EmojiPackController, :delete) - - post("/files", EmojiFileController, :add_file) - patch("/files", EmojiFileController, :update_file) - delete("/files", EmojiFileController, :delete_file) + post("/files", EmojiFileController, :create) + patch("/files", EmojiFileController, :update) + delete("/files", EmojiFileController, :delete) end # Pack info / downloading -- cgit v1.2.3 From d0078bc40406939cb584847c90b00aad006812e4 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 24 Sep 2020 15:54:55 +0200 Subject: User Search: Boost resolved results and exact ap_id matches. --- lib/pleroma/user/search.ex | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index b8c648672..408295e0c 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -19,11 +19,21 @@ defmodule Pleroma.User.Search do query_string = format_query(query_string) - maybe_resolve(resolve, for_user, query_string) + # If this returns anything, it should bounce to the top + maybe_resolved = maybe_resolve(resolve, for_user, query_string) + maybe_ap_id_match = User.get_cached_by_ap_id(query_string) + + top_user_ids = + case {maybe_resolved, maybe_ap_id_match} do + {{:ok, %User{} = user}, %User{} = other_user} -> [user.id, other_user.id] + {{:ok, %User{} = user}, _} -> [user.id] + {_, %User{} = user} -> [user.id] + _ -> [] + end results = query_string - |> search_query(for_user, following) + |> search_query(for_user, following, top_user_ids) |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) results @@ -47,7 +57,7 @@ defmodule Pleroma.User.Search do end end - defp search_query(query_string, for_user, following) do + defp search_query(query_string, for_user, following, top_user_ids) do for_user |> base_query(following) |> filter_blocked_user(for_user) @@ -56,13 +66,20 @@ defmodule Pleroma.User.Search do |> filter_internal_users() |> filter_blocked_domains(for_user) |> fts_search(query_string) + |> select_top_users(top_user_ids) |> trigram_rank(query_string) - |> boost_search_rank(for_user) + |> boost_search_rank(for_user, top_user_ids) |> subquery() |> order_by(desc: :search_rank) |> maybe_restrict_local(for_user) end + defp select_top_users(query, top_user_ids) do + from(u in query, + or_where: u.id in ^top_user_ids + ) + end + defp fts_search(query, query_string) do query_string = to_tsquery(query_string) @@ -180,7 +197,7 @@ defmodule Pleroma.User.Search do defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) - defp boost_search_rank(query, %User{} = for_user) do + defp boost_search_rank(query, %User{} = for_user, top_user_ids) do friends_ids = User.get_friends_ids(for_user) followers_ids = User.get_followers_ids(for_user) @@ -192,6 +209,7 @@ defmodule Pleroma.User.Search do CASE WHEN (?) THEN (?) * 1.5 WHEN (?) THEN (?) * 1.3 WHEN (?) THEN (?) * 1.1 + WHEN (?) THEN 9001 ELSE (?) END """, u.id in ^friends_ids and u.id in ^followers_ids, @@ -200,11 +218,26 @@ defmodule Pleroma.User.Search do u.search_rank, u.id in ^followers_ids, u.search_rank, + u.id in ^top_user_ids, u.search_rank ) } ) end - defp boost_search_rank(query, _for_user), do: query + defp boost_search_rank(query, _for_user, top_user_ids) do + from(u in subquery(query), + select_merge: %{ + search_rank: + fragment( + """ + CASE WHEN (?) THEN 9001 + ELSE (?) END + """, + u.id in ^top_user_ids, + u.search_rank + ) + } + ) + end end -- cgit v1.2.3 From 935ef21b0285975b08037827a33f32bfcbbff951 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 16:47:34 -0500 Subject: Use the import --- lib/mix/tasks/pleroma/email.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 1f543241a..0e4c87598 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Pleroma.Email do @moduledoc File.read!("docs/administration/CLI_tasks/email.md") def run(["test" | args]) do - Mix.Pleroma.start_pleroma() + start_pleroma() {options, [], []} = OptionParser.parse( -- cgit v1.2.3 From e33360fdb958708661a5bda415b9f06b5e1290d5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 18:23:47 -0500 Subject: Cannot use batches with User.try_send_confirmation_email/1 --- lib/mix/tasks/pleroma/email.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 0e4c87598..9e989ed46 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -31,7 +31,7 @@ defmodule Mix.Tasks.Pleroma.Email do confirmation_pending: true, invisible: false }) - |> Pleroma.Repo.chunk_stream(500, :batches) + |> Pleroma.Repo.chunk_stream(500) |> Stream.each(&Pleroma.User.try_send_confirmation_email(&1)) |> Stream.run() end -- cgit v1.2.3 From 81faf540b30d0ecf75779e7a5c16f42b7218cec8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 24 Sep 2020 18:35:20 -0500 Subject: Add some user feedback --- lib/mix/tasks/pleroma/email.ex | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 9e989ed46..9972cb988 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -25,6 +25,8 @@ defmodule Mix.Tasks.Pleroma.Email do def run(["resend_confirmation_emails"]) do start_pleroma() + shell_info("Sending emails to all unconfirmed users") + Pleroma.User.Query.build(%{ local: true, deactivated: false, -- cgit v1.2.3 From a8c17ea25a79491328345f5834397eb6821a77f1 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 25 Sep 2020 08:46:14 +0200 Subject: User Search: Also find user by uri --- lib/pleroma/user/search.ex | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 408295e0c..d747bfa52 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -5,6 +5,7 @@ defmodule Pleroma.User.Search do alias Pleroma.Pagination alias Pleroma.User + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType import Ecto.Query @limit 20 @@ -21,15 +22,12 @@ defmodule Pleroma.User.Search do # If this returns anything, it should bounce to the top maybe_resolved = maybe_resolve(resolve, for_user, query_string) - maybe_ap_id_match = User.get_cached_by_ap_id(query_string) top_user_ids = - case {maybe_resolved, maybe_ap_id_match} do - {{:ok, %User{} = user}, %User{} = other_user} -> [user.id, other_user.id] - {{:ok, %User{} = user}, _} -> [user.id] - {_, %User{} = user} -> [user.id] - _ -> [] - end + [] + |> maybe_add_resolved(maybe_resolved) + |> maybe_add_ap_id_match(query_string) + |> maybe_add_uri_match(query_string) results = query_string @@ -39,6 +37,29 @@ defmodule Pleroma.User.Search do results end + defp maybe_add_resolved(list, {:ok, %User{} = user}) do + [user.id | list] + end + + defp maybe_add_resolved(list, _), do: list + + defp maybe_add_ap_id_match(list, query) do + if user = User.get_cached_by_ap_id(query) do + [user.id | list] + else + list + end + end + + defp maybe_add_uri_match(list, query) do + with {:ok, query} <- UriType.cast(query), + %User{} = user <- Pleroma.Repo.get_by(User, uri: query) do + [user.id | list] + else + _ -> list + end + end + defp format_query(query_string) do # Strip the beginning @ off if there is a query query_string = String.trim_leading(query_string, "@") -- cgit v1.2.3 From 05b5241314182c5aab2907e27d4c5f46d7617f56 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 25 Sep 2020 09:12:48 +0200 Subject: Linter fixes --- lib/pleroma/user/search.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index d747bfa52..03f2c552f 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -3,9 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.Search do + alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType alias Pleroma.Pagination alias Pleroma.User - alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType + import Ecto.Query @limit 20 -- cgit v1.2.3 From 8b84ca4901c378d734cd87ae3e4bf72c508a84bf Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 25 Sep 2020 10:37:59 -0500 Subject: Simplify the value comparison --- lib/mix/tasks/pleroma/user.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index d50205600..e06262804 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -455,11 +455,7 @@ defmodule Mix.Tasks.Pleroma.User do end defp set_confirmed(user, value) do - {:ok, user} = - case value do - true -> User.need_confirmation(user, false) - false -> User.need_confirmation(user, true) - end + {:ok, user} = User.need_confirmation(user, !value) shell_info("Confirmation pending status of #{user.nickname}: #{user.confirmation_pending}") user -- cgit v1.2.3 From 4e4f77108207157a49a627edb03951e2f15b62f1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 26 Sep 2020 19:32:16 +0300 Subject: Adjusted MediaProxyControllerTest to gracefully fail on missing dependencies. Installation docs update. Added ffmpeg/imagemagick checks to launch checks (if media preview proxy is enabled). Added documentation on installing optional media / graphics packages (imagemagick, ffmpeg, exiftool). --- lib/pleroma/application.ex | 18 +++++++++++++++++- lib/pleroma/helpers/media_helper.ex | 12 ++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 00ec79a2a..d7d8e423e 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -268,7 +268,8 @@ defmodule Pleroma.Application do with true <- filter in filters, false <- Pleroma.Utils.command_available?(command_required) do Logger.error( - "#{filter} is specified in list of Pleroma.Upload filters, but the #{command_required} command is not found" + "#{filter} is specified in list of Pleroma.Upload filters, but the " <> + "#{command_required} command is not found" ) end end @@ -276,5 +277,20 @@ defmodule Pleroma.Application do check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool") check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify") check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify") + + with true <- Config.get([:media_preview_proxy, :enabled]), + missing_graphics_tools = Pleroma.Helpers.MediaHelper.missing_dependencies(), + [] <- missing_graphics_tools do + :noop + else + false -> + :noop + + missing_graphics_tools -> + Logger.error( + "The following dependencies required by Media preview proxy " <> + "(which is currently enabled) are not installed: #{inspect(missing_graphics_tools)}" + ) + end end end diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index b6f35a24b..6b799173e 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -9,6 +9,18 @@ defmodule Pleroma.Helpers.MediaHelper do alias Pleroma.HTTP + require Logger + + def missing_dependencies do + Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc -> + if Pleroma.Utils.command_available?(executable) do + acc + else + [sym | acc] + end + end) + end + def image_resize(url, options) do with executable when is_binary(executable) <- System.find_executable("convert"), {:ok, args} <- prepare_image_resize_args(options), -- cgit v1.2.3 From de993b856bc2145e7c4aaa47767c7edc826798c7 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 28 Sep 2020 09:16:42 +0300 Subject: added `force` option to the unfollow operation --- lib/mix/tasks/pleroma/relay.ex | 13 +++++++++++-- lib/pleroma/user.ex | 4 +--- lib/pleroma/web/activity_pub/relay.ex | 20 ++++++++++++++++---- .../web/admin_api/controllers/relay_controller.ex | 16 ++++------------ .../web/api_spec/operations/admin/relay_operation.ex | 12 +++++++++++- 5 files changed, 43 insertions(+), 22 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index a6d8d6c1c..bb808ca47 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -21,10 +21,19 @@ defmodule Mix.Tasks.Pleroma.Relay do end end - def run(["unfollow", target]) do + def run(["unfollow", target | rest]) do start_pleroma() - with {:ok, _activity} <- Relay.unfollow(target) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [force: :boolean], + aliases: [f: :force] + ) + + force = Keyword.get(options, :force, false) + + with {:ok, _activity} <- Relay.unfollow(target, %{force: force}) do # put this task to sleep to allow the genserver to push out the messages :timer.sleep(500) else diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 03be61ccf..71ace1c34 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -915,9 +915,7 @@ defmodule Pleroma.User do FollowingRelationship.unfollow(follower, followed) {:ok, followed} = update_follower_count(followed) - {:ok, follower} = - follower - |> update_following_count() + {:ok, follower} = update_following_count(follower) {:ok, follower, followed} diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index b65710a94..6606e1780 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -30,12 +30,16 @@ defmodule Pleroma.Web.ActivityPub.Relay do end end - @spec unfollow(String.t()) :: {:ok, Activity.t()} | {:error, any()} - def unfollow(target_instance) do + @spec unfollow(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()} + def unfollow(target_instance, opts \\ %{}) do with %User{} = local_user <- get_actor(), - {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), + {:ok, target_user} <- fetch_target_user(target_instance, opts), {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do - User.unfollow(local_user, target_user) + case target_user.id do + nil -> User.update_following_count(local_user) + _ -> User.unfollow(local_user, target_user) + end + Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} else @@ -43,6 +47,14 @@ defmodule Pleroma.Web.ActivityPub.Relay do end end + defp fetch_target_user(ap_id, opts) do + case {opts[:force], User.get_or_fetch_by_ap_id(ap_id)} do + {_, {:ok, %User{} = user}} -> {:ok, user} + {true, _} -> {:ok, %User{ap_id: ap_id}} + {_, error} -> error + end + end + @spec publish(any()) :: {:ok, Activity.t()} | {:error, any()} def publish(%Activity{data: %{"type" => "Create"}} = activity) do with %User{} = user <- get_actor(), diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex index 95d06dde7..6c19f09f7 100644 --- a/lib/pleroma/web/admin_api/controllers/relay_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -33,11 +33,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do with {:ok, _message} <- Relay.follow(target) do - ModerationLog.insert_log(%{ - action: "relay_follow", - actor: admin, - target: target - }) + ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target}) json(conn, %{actor: target, followed_back: target in Relay.following()}) else @@ -48,13 +44,9 @@ defmodule Pleroma.Web.AdminAPI.RelayController do end end - def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do - with {:ok, _message} <- Relay.unfollow(target) do - ModerationLog.insert_log(%{ - action: "relay_unfollow", - actor: admin, - target: target - }) + def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do + with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do + ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target}) json(conn, target) else diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex index e06b2d164..f754bb9f5 100644 --- a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -56,7 +56,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do operationId: "AdminAPI.RelayController.unfollow", security: [%{"oAuth" => ["write:follows"]}], parameters: admin_api_params(), - requestBody: request_body("Parameters", relay_url()), + requestBody: request_body("Parameters", relay_unfollow()), responses: %{ 200 => Operation.response("Status", "application/json", %Schema{ @@ -91,4 +91,14 @@ defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do } } end + + defp relay_unfollow do + %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri}, + force: %Schema{type: :boolean, default: false} + } + } + end end -- cgit v1.2.3 From ba7f9459b4798388eb4e441d096302c018354033 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 28 Sep 2020 18:22:59 -0500 Subject: Revert Rich Media censorship for sensitive statuses The #NSFW hashtag test was broken anyway. --- lib/pleroma/web/rich_media/helpers.ex | 1 - 1 file changed, 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index d7a19df4a..d67b594b5 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -57,7 +57,6 @@ defmodule Pleroma.Web.RichMedia.Helpers do def fetch_data_for_object(object) do with true <- Config.get([:rich_media, :enabled]), - false <- object.data["sensitive"] || false, {:ok, page_url} <- HTML.extract_first_external_url_from_object(object), :ok <- validate_page_url(page_url), -- cgit v1.2.3 From 7d5c3883acafc2c84f65f38dd639d4999f14215a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 29 Sep 2020 16:28:06 +0300 Subject: [#3031] Refactoring: moved system commands checks to ApplicationRequirements. --- lib/pleroma/application.ex | 34 ---------------- lib/pleroma/application_requirements.ex | 72 ++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 39 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index d7d8e423e..e73d89350 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -56,7 +56,6 @@ defmodule Pleroma.Application do Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() - check_system_commands() Pleroma.Docs.JSON.compile() adapter = Application.get_env(:tesla, :adapter) @@ -260,37 +259,4 @@ defmodule Pleroma.Application do end defp http_children(_, _), do: [] - - defp check_system_commands do - filters = Config.get([Pleroma.Upload, :filters]) - - check_filter = fn filter, command_required -> - with true <- filter in filters, - false <- Pleroma.Utils.command_available?(command_required) do - Logger.error( - "#{filter} is specified in list of Pleroma.Upload filters, but the " <> - "#{command_required} command is not found" - ) - end - end - - check_filter.(Pleroma.Upload.Filters.Exiftool, "exiftool") - check_filter.(Pleroma.Upload.Filters.Mogrify, "mogrify") - check_filter.(Pleroma.Upload.Filters.Mogrifun, "mogrify") - - with true <- Config.get([:media_preview_proxy, :enabled]), - missing_graphics_tools = Pleroma.Helpers.MediaHelper.missing_dependencies(), - [] <- missing_graphics_tools do - :noop - else - false -> - :noop - - missing_graphics_tools -> - Logger.error( - "The following dependencies required by Media preview proxy " <> - "(which is currently enabled) are not installed: #{inspect(missing_graphics_tools)}" - ) - end - end end diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex index 16f62b6f5..b977257a3 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -9,6 +9,9 @@ defmodule Pleroma.ApplicationRequirements do defmodule VerifyError, do: defexception([:message]) + alias Pleroma.Config + alias Pleroma.Helpers.MediaHelper + import Ecto.Query require Logger @@ -16,7 +19,8 @@ defmodule Pleroma.ApplicationRequirements do @spec verify!() :: :ok | VerifyError.t() def verify! do :ok - |> check_confirmation_accounts! + |> check_system_commands!() + |> check_confirmation_accounts!() |> check_migrations_applied!() |> check_welcome_message_config!() |> check_rum!() @@ -48,7 +52,9 @@ defmodule Pleroma.ApplicationRequirements do if Pleroma.Config.get([:instance, :account_activation_required]) && not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do Logger.error( - "Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer." + "Account activation enabled, but no Mailer settings enabled.\n" <> + "Please set config :pleroma, :instance, account_activation_required: false\n" <> + "Otherwise setup and enable Mailer." ) {:error, @@ -81,7 +87,9 @@ defmodule Pleroma.ApplicationRequirements do Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end) Logger.error( - "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" + "The following migrations were not applied:\n#{down_migrations_text}" <> + "If you want to start Pleroma anyway, set\n" <> + "config :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" ) {:error, "Unapplied Migrations detected"} @@ -124,14 +132,22 @@ defmodule Pleroma.ApplicationRequirements do case {setting, migrate} do {true, false} -> Logger.error( - "Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`" + "Use `RUM` index is enabled, but were not applied migrations for it.\n" <> + "If you want to start Pleroma anyway, set\n" <> + "config :pleroma, :database, rum_enabled: false\n" <> + "Otherwise apply the following migrations:\n" <> + "`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`" ) {:error, "Unapplied RUM Migrations detected"} {false, true} -> Logger.error( - "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`" + "Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\n" <> + "If you want to use `RUM`, set\n" <> + "config :pleroma, :database, rum_enabled: true\n" <> + "Otherwise roll `RUM` migrations back.\n" <> + "`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`" ) {:error, "RUM Migrations detected"} @@ -140,4 +156,50 @@ defmodule Pleroma.ApplicationRequirements do :ok end end + + defp check_system_commands!(:ok) do + filter_commands_statuses = [ + check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"), + check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"), + check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify") + ] + + preview_proxy_commands_status = + if !Config.get([:media_preview_proxy, :enabled]) or + MediaHelper.missing_dependencies() == [] do + true + else + Logger.error( + "The following dependencies required by Media preview proxy " <> + "(which is currently enabled) are not installed: " <> + inspect(MediaHelper.missing_dependencies()) + ) + + false + end + + if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do + :ok + else + {:error, + "System commands missing. Check logs and see `docs/installation` for more details."} + end + end + + defp check_system_commands!(result), do: result + + defp check_filter(filter, command_required) do + filters = Config.get([Pleroma.Upload, :filters]) + + if filter in filters and not Pleroma.Utils.command_available?(command_required) do + Logger.error( + "#{filter} is specified in list of Pleroma.Upload filters, but the " <> + "#{command_required} command is not found" + ) + + false + else + true + end + end end -- cgit v1.2.3 From b3a9ba09ec5867d240c3769ae4c3fbf598f68d92 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 29 Sep 2020 12:16:15 -0500 Subject: More robust expires_at timestamp processing --- lib/pleroma/user.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 71ace1c34..09ea80793 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2291,7 +2291,9 @@ defmodule Pleroma.User do # if pinned activity was scheduled for deletion, we reschedule it for deletion if data["expires_at"] do - {:ok, expires_at, _} = DateTime.from_iso8601(data["expires_at"]) + # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation + {:ok, expires_at} = + data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast() Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ activity_id: id, -- cgit v1.2.3 From 006b62fd12adadbf698419990ab13bf6f1e901b2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 29 Sep 2020 21:49:04 +0300 Subject: OpenAPI CastAndValidate: filter out empty params Closes #2198 --- lib/pleroma/web/api_spec/cast_and_validate.ex | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index fbfc27d6f..6d1a7ebbc 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -115,6 +115,10 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do %{reason: :unexpected_field, name: name, path: [name]}, params -> Map.delete(params, name) + # Filter out empty params + %{reason: :invalid_type, path: [name_atom], value: ""}, params -> + Map.delete(params, to_string(name_atom)) + %{reason: :invalid_enum, name: nil, path: path, value: value}, params -> path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string() update_in(params, path, &List.delete(&1, value)) -- cgit v1.2.3 From 90fee49c52799a7d6ad890ecc49d146ab6ad8455 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 30 Sep 2020 14:14:41 +0200 Subject: User search: Once again, change uri handling. They can indeed be non-unique. --- lib/pleroma/user/search.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 03f2c552f..35a828008 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -54,8 +54,9 @@ defmodule Pleroma.User.Search do defp maybe_add_uri_match(list, query) do with {:ok, query} <- UriType.cast(query), - %User{} = user <- Pleroma.Repo.get_by(User, uri: query) do - [user.id | list] + q = from(u in User, where: u.uri == ^query, select: u.id), + users = Pleroma.Repo.all(q) do + users ++ list else _ -> list end -- cgit v1.2.3 From cbdaabad345914e7424e614032056ff86e21142f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 1 Oct 2020 13:32:11 +0300 Subject: web push http_client fix --- lib/pleroma/http/web_push.ex | 12 ++++++++++++ lib/pleroma/web/push/impl.ex | 12 ++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 lib/pleroma/http/web_push.ex (limited to 'lib') diff --git a/lib/pleroma/http/web_push.ex b/lib/pleroma/http/web_push.ex new file mode 100644 index 000000000..78148a12e --- /dev/null +++ b/lib/pleroma/http/web_push.ex @@ -0,0 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.WebPush do + @moduledoc false + + def post(url, payload, headers) do + list_headers = Map.to_list(headers) + Pleroma.HTTP.post(url, payload, list_headers) + end +end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 16368485e..da535aa68 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.Push.Impl do @types ["Create", "Follow", "Announce", "Like", "Move"] @doc "Performs sending notifications for user subscriptions" - @spec perform(Notification.t()) :: list(any) | :error + @spec perform(Notification.t()) :: list(any) | :error | {:error, :unknown_type} def perform( %{ activity: %{data: %{"type" => activity_type}} = activity, @@ -64,20 +64,20 @@ defmodule Pleroma.Web.Push.Impl do @doc "Push message to web" def push_message(body, sub, api_key, subscription) do case WebPushEncryption.send_web_push(body, sub, api_key) do - {:ok, %{status_code: code}} when 400 <= code and code < 500 -> + {:ok, %{status: code}} when code in 400..499 -> Logger.debug("Removing subscription record") Repo.delete!(subscription) :ok - {:ok, %{status_code: code}} when 200 <= code and code < 300 -> + {:ok, %{status: code}} when code in 200..299 -> :ok - {:ok, %{status_code: code}} -> + {:ok, %{status: code}} -> Logger.error("Web Push Notification failed with code: #{code}") :error - _ -> - Logger.error("Web Push Notification failed with unknown error") + error -> + Logger.error("Web Push Notification failed with #{inspect(error)}") :error end end -- cgit v1.2.3 From 7efadc3cbd46369e960f31c33a2c555f718ca8c5 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 1 Oct 2020 21:34:45 +0300 Subject: No auth check in OStatusController, even on non-federating instances. --- lib/pleroma/web/ostatus/ostatus_controller.ex | 4 ---- 1 file changed, 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index de1b0b3f0..8646d2c1c 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -16,10 +16,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Router - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 - ) - plug( RateLimiter, [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] -- cgit v1.2.3 From 0d575735bfd280b878bdecc6d018d8cca23ad09f Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 1 Oct 2020 21:41:22 +0300 Subject: No auth check in UserController.feed_redirect/2, even on non-federating instances. --- lib/pleroma/web/feed/user_controller.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 71eb1ea7e..09ecdedb4 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -23,12 +23,7 @@ defmodule Pleroma.Web.Feed.UserController do def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do - with %{halted: false} = conn <- - Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 - ) do - ActivityPubController.call(conn, :user) - end + ActivityPubController.call(conn, :user) end def feed_redirect(conn, %{"nickname" => nickname}) do -- cgit v1.2.3 From f6024252ae8601d41bea943bb3cae5c656416eb9 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Fri, 2 Oct 2020 22:18:02 +0300 Subject: [#3053] No auth check in StaticFEController, even on non-federating instances. Adjusted tests. --- lib/pleroma/web/feed/tag_controller.ex | 4 +- lib/pleroma/web/feed/user_controller.ex | 4 +- lib/pleroma/web/router.ex | 11 +- lib/pleroma/web/static_fe/static_fe_controller.ex | 124 +++++++++++----------- 4 files changed, 75 insertions(+), 68 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 93a8294b7..c348b32c2 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -10,14 +10,14 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.Feed.FeedView def feed(conn, params) do - unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do + unless Config.restrict_unauthenticated_access?(:activities, :local) do render_feed(conn, params) else render_error(conn, :not_found, "Not found") 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) activities = diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 09ecdedb4..5fbcd82d7 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -40,11 +40,11 @@ defmodule Pleroma.Web.Feed.UserController do end end - def render_feed(conn, %{"nickname" => nickname} = params) do + defp render_feed(conn, %{"nickname" => nickname} = params) do format = get_format(conn) format = - if format in ["rss", "atom"] do + if format in ["atom", "rss"] do format else "atom" diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 42a9db21d..e0e92549f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -561,12 +561,17 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.StaticFEPlug) end + pipeline :ostatus_no_html do + plug(:accepts, ["xml", "rss", "atom", "activity+json", "json"]) + end + pipeline :oembed do plug(:accepts, ["json", "xml"]) end scope "/", Pleroma.Web do - pipe_through([:ostatus, :http_signature]) + # Note: no authentication plugs, all endpoints below should only yield public objects + pipe_through(:ostatus) get("/objects/:uuid", OStatus.OStatusController, :object) get("/activities/:uuid", OStatus.OStatusController, :activity) @@ -579,6 +584,10 @@ defmodule Pleroma.Web.Router do 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(:ostatus_no_html) get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed) end diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index a7a891b13..b1c62f5b0 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -17,70 +17,9 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:assign_id) - plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/1 - ) - @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 - - defp 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 - - defp represent(%Activity{} = activity), do: represent(activity, false) - - defp 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 - - content = - if data["content"] do - data["content"] - |> Pleroma.HTML.filter_tags() - |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) - else - nil - end - - %{ - user: User.sanitize_html(user), - title: get_title(activity.object), - content: content, - attachment: data["attachment"], - link: link, - published: data["published"], - sensitive: data["sensitive"], - selected: selected, - counts: get_counts(activity), - id: activity.id - } - end - + @doc "Renders requested local public activity" def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do with %Activity{local: true} = activity <- Activity.get_by_id_with_object(notice_id), @@ -106,6 +45,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do end end + @doc "Renders public activities of requested user" 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 -> @@ -118,7 +58,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do timeline = user - |> ActivityPub.fetch_user_activities(nil, params) + |> ActivityPub.fetch_user_activities(_reading_user = nil, params) |> Enum.map(&represent/1) prev_page_id = @@ -166,6 +106,64 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do end end + 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 + + defp 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 + + defp represent(%Activity{} = activity), do: represent(activity, false) + + defp 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 + + content = + if data["content"] do + data["content"] + |> Pleroma.HTML.filter_tags() + |> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{})) + else + nil + end + + %{ + user: User.sanitize_html(user), + title: get_title(activity.object), + content: content, + attachment: data["attachment"], + link: link, + published: data["published"], + sensitive: data["sensitive"], + selected: selected, + counts: get_counts(activity), + id: activity.id + } + end + defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), do: assign(conn, :notice_id, notice_id) -- cgit v1.2.3 From 35ee759e74d0737598311d8e4245168f981812d3 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 5 Oct 2020 11:48:41 -0500 Subject: Add helper function to convert single IPs into CIDR format if they were not provided that way --- lib/pleroma/plugs/remote_ip.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 0ac9050d0..d1b1f793a 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -47,8 +47,19 @@ defmodule Pleroma.Plugs.RemoteIp do config |> Keyword.get(:proxies, []) |> Enum.concat(reserved) - |> Enum.map(&InetCidr.parse/1) + |> Enum.map(&maybe_add_cidr/1) {headers, proxies} end + + defp maybe_add_cidr(proxy) when is_binary(proxy) do + proxy = + cond do + "/" in String.codepoints(proxy) -> proxy + InetCidr.v4?(InetCidr.parse_address!(proxy)) -> proxy <> "/32" + InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" + end + + InetCidr.parse(proxy) + end end -- cgit v1.2.3 From f497eb034df6647fef9086a6e2ef03e61e2efc47 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 5 Oct 2020 21:11:00 +0200 Subject: activity_pub_controller.ex: Remove unused @doc block [ci skip] --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 732c44271..832155643 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -523,19 +523,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {new_user, for_user} end - @doc """ - Endpoint based on - - Parameters: - - (required) `file`: data of the media - - (optionnal) `description`: description of the media, intended for accessibility - - Response: - - HTTP Code: 201 Created - - HTTP Body: ActivityPub object to be inserted into another's `attachment` field - - Note: Will not point to a URL with a `Location` header because no standalone Activity has been created. - """ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do with {:ok, object} <- ActivityPub.upload( -- cgit v1.2.3 From 094edde7c4ddf65f46e5d692a5ef5b859587d1e1 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 5 Oct 2020 23:48:00 +0300 Subject: [#3053] Unauthenticated access control for OStatus-related controllers and ActivityPubController (base actions: :user, :object, :activity). Tests adjustments. --- .../web/activity_pub/activity_pub_controller.ex | 56 ++++++++++++---------- lib/pleroma/web/activity_pub/visibility.ex | 39 +++++++++++---- lib/pleroma/web/feed/tag_controller.ex | 15 +++--- lib/pleroma/web/feed/user_controller.ex | 19 ++++---- lib/pleroma/web/ostatus/ostatus_controller.ex | 26 +++++----- lib/pleroma/web/router.ex | 46 +++++++++++++----- lib/pleroma/web/static_fe/static_fe_controller.ex | 56 +++++++++++----------- 7 files changed, 154 insertions(+), 103 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 732c44271..c78edfb4c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -32,17 +32,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers] + # Note: :following and :followers must be served even without authentication (as via :api) + @auth_only_actions [:read_inbox, :update_outbox, :whoami, :upload_media] + + # Always accessible actions (must perform entity accessibility checks) + @no_auth_no_federation_actions [:user, :activity, :object] + + @authenticated_or_federating_actions @federating_only_actions ++ + @auth_only_actions ++ @no_auth_no_federation_actions + plug(FederatingPlug when action in @federating_only_actions) - plug( - EnsureAuthenticatedPlug, - [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions - ) + plug(EnsureAuthenticatedPlug when action in @auth_only_actions) - # Note: :following and :followers must be served even without authentication (as via :api) plug( - EnsureAuthenticatedPlug - when action in [:read_inbox, :update_outbox, :whoami, :upload_media] + EnsureAuthenticatedPlug, + [unless_func: &FederatingPlug.federating?/1] + when action not in @authenticated_or_federating_actions ) plug( @@ -66,21 +72,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def user(conn, %{"nickname" => nickname}) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname), + {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)}, {:ok, user} <- User.ensure_keys_present(user) do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("user.json", %{user: user}) else - nil -> {:error, :not_found} - %{local: false} -> {:error, :not_found} + _ -> {:error, :not_found} end end def object(conn, _) do with ap_id <- Endpoint.url() <> conn.request_path, %Object{} = object <- Object.get_cached_by_ap_id(ap_id), - {_, true} <- {:public?, Visibility.is_public?(object)} do + {_, true} <- {:public?, Visibility.is_public?(object)}, + {_, false} <- {:restricted?, Visibility.restrict_unauthenticated_access?(object)} do conn |> assign(:tracking_fun_data, object.id) |> set_cache_ttl_for(object) @@ -88,25 +95,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_view(ObjectView) |> render("object.json", object: object) else - {:public?, false} -> - {:error, :not_found} + _ -> {:error, :not_found} end end - def track_object_fetch(conn, nil), do: conn - - def track_object_fetch(conn, object_id) do - with %{assigns: %{user: %User{id: user_id}}} <- conn do - Delivery.create(object_id, user_id) - end - - conn - end - def activity(conn, _params) do with ap_id <- Endpoint.url() <> conn.request_path, %Activity{} = activity <- Activity.normalize(ap_id), - {_, true} <- {:public?, Visibility.is_public?(activity)} do + {_, true} <- {:public?, Visibility.is_public?(activity)}, + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)} do conn |> maybe_set_tracking_data(activity) |> set_cache_ttl_for(activity) @@ -114,8 +111,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_view(ObjectView) |> render("object.json", object: activity) else - {:public?, false} -> {:error, :not_found} - nil -> {:error, :not_found} + _ -> {:error, :not_found} end end @@ -550,4 +546,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(object.data) end end + + def track_object_fetch(conn, nil), do: conn + + def track_object_fetch(conn, object_id) do + with %{assigns: %{user: %User{id: user_id}}} <- conn do + Delivery.create(object_id, user_id) + end + + conn + end end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 5c349bb7a..76bd54a42 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -44,29 +44,30 @@ defmodule Pleroma.Web.ActivityPub.Visibility do def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(_), do: false - @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() - def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean() + 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?(%{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"] || list_ap_id |> Pleroma.List.get_by_ap_id() |> Pleroma.List.member?(user) end - def visible_for_user?(%{local: local} = activity, nil) do - cfg_key = if local, do: :local, else: :remote - - if Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key), + def visible_for_user?(%Activity{} = activity, nil) do + if restrict_unauthenticated_access?(activity), do: false, else: is_public?(activity) end - def visible_for_user?(activity, user) do + def visible_for_user?(%Activity{} = activity, user) do x = [user.ap_id | User.following(user)] y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) is_public?(activity) || Enum.any?(x, &(&1 in y)) @@ -82,6 +83,26 @@ defmodule Pleroma.Web.ActivityPub.Visibility do result 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 to = object.data["to"] || [] cc = object.data["cc"] || [] diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index c348b32c2..218cdbdf3 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.Feed.FeedView def feed(conn, params) do - unless Config.restrict_unauthenticated_access?(:activities, :local) do + if Config.get!([:instance, :public]) do render_feed(conn, params) else render_error(conn, :not_found, "Not found") @@ -36,12 +36,13 @@ defmodule Pleroma.Web.Feed.TagController do end @spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()} - defp parse_tag(raw_tag) when is_binary(raw_tag) do - case Enum.reverse(String.split(raw_tag, ".")) do - [format | tag] when format in ["atom", "rss"] -> {format, Enum.join(tag, ".")} - _ -> {"rss", raw_tag} + defp parse_tag(raw_tag) do + case is_binary(raw_tag) && Enum.reverse(String.split(raw_tag, ".")) do + [format | tag] when format in ["rss", "atom"] -> + {format, Enum.join(tag, ".")} + + _ -> + {"atom", raw_tag} end end - - defp parse_tag(raw_tag), do: {"rss", raw_tag} end diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 5fbcd82d7..f1d2bb7be 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.Feed.UserController do use Pleroma.Web, :controller alias Fallback.RedirectController + + alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPubController @@ -32,15 +34,7 @@ defmodule Pleroma.Web.Feed.UserController do end end - def feed(conn, params) do - unless Pleroma.Config.restrict_unauthenticated_access?(:profiles, :local) do - render_feed(conn, params) - else - errors(conn, {:error, :not_found}) - end - end - - defp render_feed(conn, %{"nickname" => nickname} = params) do + def feed(conn, %{"nickname" => nickname} = params) do format = get_format(conn) format = @@ -50,7 +44,8 @@ defmodule Pleroma.Web.Feed.UserController do "atom" 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 = %{ type: ["Create"], @@ -65,7 +60,7 @@ defmodule Pleroma.Web.Feed.UserController do |> render("user.#{format}", user: user, activities: activities, - feed_config: Pleroma.Config.get([:feed]) + feed_config: Config.get([:feed]) ) end end @@ -77,6 +72,8 @@ defmodule Pleroma.Web.Feed.UserController do 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, {:visibility, _}), do: errors(conn, {:error, :not_found}) + def errors(conn, _) do render_error(conn, :internal_server_error, "Something went wrong") end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 8646d2c1c..b4dc2a87f 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -33,16 +33,15 @@ defmodule Pleroma.Web.OStatus.OStatusController do ActivityPubController.call(conn, :object) end - def object(%{assigns: %{format: format}} = conn, _params) do + def object(conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end + {_, true} <- {:public?, Visibility.is_public?(activity)}, + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)} do + redirect(conn, to: "/notice/#{activity.id}") else - reason when reason in [{:public?, false}, {:activity, nil}] -> + reason when reason in [{:public?, false}, {:visible?, false}, {:activity, nil}] -> {:error, :not_found} e -> @@ -55,15 +54,14 @@ defmodule Pleroma.Web.OStatus.OStatusController do ActivityPubController.call(conn, :activity) end - def activity(%{assigns: %{format: format}} = conn, _params) do + def activity(conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)} do - case format do - _ -> redirect(conn, to: "/notice/#{activity.id}") - end + {_, true} <- {:public?, Visibility.is_public?(activity)}, + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)} do + redirect(conn, to: "/notice/#{activity.id}") else - reason when reason in [{:public?, false}, {:activity, nil}] -> + reason when reason in [{:public?, false}, {:visible?, false}, {:activity, nil}] -> {:error, :not_found} e -> @@ -74,6 +72,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)}, + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do cond do format in ["json", "activity+json"] -> @@ -101,7 +100,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do RedirectController.redirector(conn, nil) end else - reason when reason in [{:public?, false}, {:activity, nil}] -> + reason when reason in [{:public?, false}, {:visible?, false}, {:activity, nil}] -> conn |> put_status(404) |> RedirectController.redirector(nil, 404) @@ -115,6 +114,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do def notice_player(conn, %{"id" => id}) do with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), true <- Visibility.is_public?(activity), + {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %Object{} = object <- Object.normalize(activity), %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e0e92549f..6439a1c39 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -5,6 +5,14 @@ defmodule Pleroma.Web.Router do use Pleroma.Web, :router + pipeline :accepts_html do + plug(:accepts, ["html"]) + end + + pipeline :accepts_xml_rss_atom do + plug(:accepts, ["xml", "rss", "atom"]) + end + pipeline :browser do plug(:accepts, ["html"]) plug(:fetch_session) @@ -556,39 +564,55 @@ defmodule Pleroma.Web.Router do ) end - pipeline :ostatus do - plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) + pipeline :ostatus_html_json do + plug(:accepts, ["html", "activity+json", "json"]) plug(Pleroma.Plugs.StaticFEPlug) end - pipeline :ostatus_no_html do - plug(:accepts, ["xml", "rss", "atom", "activity+json", "json"]) + pipeline :ostatus_html_xml do + plug(:accepts, ["html", "xml", "rss", "atom"]) + plug(Pleroma.Plugs.StaticFEPlug) end - pipeline :oembed do - plug(:accepts, ["json", "xml"]) + pipeline :ostatus_html_xml_json do + plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) + plug(Pleroma.Plugs.StaticFEPlug) end scope "/", Pleroma.Web do - # Note: no authentication plugs, all endpoints below should only yield public objects - pipe_through(:ostatus) + # Note: html format is supported only if static FE is enabled + pipe_through(:ostatus_html_json) get("/objects/:uuid", OStatus.OStatusController, :object) get("/activities/:uuid", OStatus.OStatusController, :activity) get("/notice/:id", OStatus.OStatusController, :notice) - get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player) # Mastodon compatibility routes get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object) get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity) + end - get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) + scope "/", Pleroma.Web do + # Note: html format is supported only if static FE is enabled + pipe_through(:ostatus_html_xml_json) + + # Note: for json format responds with user profile (not user feed) get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) end scope "/", Pleroma.Web do - pipe_through(:ostatus_no_html) + # Note: html format is supported only if static FE is enabled + pipe_through(:ostatus_html_xml) + get("/users/:nickname/feed", Feed.UserController, :feed, 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) end diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index b1c62f5b0..76b82589f 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -24,6 +24,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController 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}) @@ -47,34 +48,35 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do @doc "Renders public activities of requested user" 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(_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 - }) + 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 -- cgit v1.2.3 From d43d05005ae4e8b0f069111baee867492d4f0c52 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:02:46 -0500 Subject: Move hardcoded default configuration into config.exs --- lib/pleroma/plugs/remote_ip.ex | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index d1b1f793a..9487efa5f 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -7,45 +7,28 @@ defmodule Pleroma.Plugs.RemoteIp do This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. """ + alias Pleroma.Config import Plug.Conn @behaviour Plug - @headers ~w[ - x-forwarded-for - ] - - # https://en.wikipedia.org/wiki/Localhost - # https://en.wikipedia.org/wiki/Private_network - @reserved ~w[ - 127.0.0.0/8 - ::1/128 - fc00::/7 - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - ] - def init(_), do: nil def call(%{remote_ip: original_remote_ip} = conn, _) do - config = Pleroma.Config.get(__MODULE__, []) - - if Keyword.get(config, :enabled, false) do - %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config)) + if Config.get([__MODULE__, :enabled]) do + %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts()) assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip) else conn end end - defp remote_ip_opts(config) do - headers = config |> Keyword.get(:headers, @headers) |> MapSet.new() - reserved = Keyword.get(config, :reserved, @reserved) + defp remote_ip_opts() do + headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() + reserved = Config.get([__MODULE__, :reserved], []) proxies = - config - |> Keyword.get(:proxies, []) + Config.get([__MODULE__, :proxies], []) |> Enum.concat(reserved) |> Enum.map(&maybe_add_cidr/1) -- cgit v1.2.3 From 7a2ed2fc90dd16a5ef45c4dd44a6e09bba035299 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Tue, 6 Oct 2020 17:26:31 -0500 Subject: Credo --- lib/pleroma/plugs/remote_ip.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 9487efa5f..51cc87ad8 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Plugs.RemoteIp do end end - defp remote_ip_opts() do + defp remote_ip_opts do headers = Config.get([__MODULE__, :headers], []) |> MapSet.new() reserved = Config.get([__MODULE__, :reserved], []) -- cgit v1.2.3 From 257e059e61b89752bcde9544cb5ae645b167c96b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 19 Aug 2020 15:31:33 +0400 Subject: Add account export --- lib/pleroma/export.ex | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 lib/pleroma/export.ex (limited to 'lib') diff --git a/lib/pleroma/export.ex b/lib/pleroma/export.ex new file mode 100644 index 000000000..82a4b7ace --- /dev/null +++ b/lib/pleroma/export.ex @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Export do + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.UserView + + import Ecto.Query + + def run(user) do + with {:ok, dir} <- create_dir(), + :ok <- actor(dir, user), + :ok <- statuses(dir, user), + :ok <- likes(dir, user), + :ok <- bookmarks(dir, user) do + IO.inspect({"DONE", dir}) + else + err -> IO.inspect({"export error", err}) + end + end + + def actor(dir, user) do + with {:ok, json} <- + UserView.render("user.json", %{user: user}) + |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) + |> Jason.encode() do + File.write(dir <> "/actor.json", json) + end + end + + defp create_dir do + datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now()) + dir = Path.join(System.tmp_dir!(), "archive-" <> datetime) + + with :ok <- File.mkdir(dir), do: {:ok, dir} + end + + defp write_header(file, name) do + IO.write( + file, + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "#{name}.json", + "type": "OrderedCollection", + "orderedItems": [ + """ + ) + end + + defp write(query, dir, name, fun) do + path = dir <> "/#{name}.json" + + with {:ok, file} <- File.open(path, [:write, :utf8]), + :ok <- write_header(file, name) do + counter = :counters.new(1, []) + + query + |> Pleroma.RepoStreamer.chunk_stream(100) + |> Stream.each(fn items -> + Enum.each(items, fn i -> + with {:ok, str} <- fun.(i), + :ok <- IO.write(file, str <> ",\n") do + :counters.add(counter, 1, 1) + end + end) + end) + |> Stream.run() + + total = :counters.get(counter, 1) + + with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do + File.close(file) + end + end + end + + def bookmarks(dir, %{id: user_id} = _user) do + Bookmark + |> where(user_id: ^user_id) + |> join(:inner, [b], activity in assoc(b, :activity)) + |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) + |> write(dir, "bookmarks", fn a -> {:ok, "\"#{a.object}\""} end) + end + + def likes(dir, user) do + user.ap_id + |> Activity.Queries.by_actor() + |> Activity.Queries.by_type("Like") + |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) + |> write(dir, "likes", fn a -> {:ok, "\"#{a.object}\""} end) + end + + def statuses(dir, user) do + opts = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + [[user.ap_id], User.following(user), Pleroma.List.memberships(user)] + |> Enum.concat() + |> ActivityPub.fetch_activities_query(opts) + |> write(dir, "outbox", fn a -> + with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do + activity |> Map.delete("@context") |> Jason.encode() + end + end) + end +end -- cgit v1.2.3 From 9d564ffc2988f145bc9cf26477eea93b1bf01cb0 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 24 Aug 2020 20:59:57 +0400 Subject: Zip exported files --- lib/pleroma/export.ex | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/export.ex b/lib/pleroma/export.ex index 82a4b7ace..f0f1ef093 100644 --- a/lib/pleroma/export.ex +++ b/lib/pleroma/export.ex @@ -12,15 +12,17 @@ defmodule Pleroma.Export do import Ecto.Query + @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] + def run(user) do - with {:ok, dir} <- create_dir(), - :ok <- actor(dir, user), - :ok <- statuses(dir, user), - :ok <- likes(dir, user), - :ok <- bookmarks(dir, user) do - IO.inspect({"DONE", dir}) - else - err -> IO.inspect({"export error", err}) + with {:ok, path} <- create_dir(user), + :ok <- actor(path, user), + :ok <- statuses(path, user), + :ok <- likes(path, user), + :ok <- bookmarks(path, user), + {:ok, zip_path} <- :zip.create('#{path}.zip', @files, cwd: path), + {:ok, _} <- File.rm_rf(path) do + {:ok, zip_path} end end @@ -33,9 +35,9 @@ defmodule Pleroma.Export do end end - defp create_dir do + defp create_dir(user) do datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now()) - dir = Path.join(System.tmp_dir!(), "archive-" <> datetime) + dir = Path.join(System.tmp_dir!(), "archive-#{user.id}-#{datetime}") with :ok <- File.mkdir(dir), do: {:ok, dir} end -- cgit v1.2.3 From c01a81804835fb92c145b90e3a264c5d4cf9c886 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 25 Aug 2020 18:51:09 +0400 Subject: Add tests --- lib/pleroma/export.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/export.ex b/lib/pleroma/export.ex index f0f1ef093..45b8ce749 100644 --- a/lib/pleroma/export.ex +++ b/lib/pleroma/export.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Export do end end - def actor(dir, user) do + defp actor(dir, user) do with {:ok, json} <- UserView.render("user.json", %{user: user}) |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) @@ -82,7 +82,7 @@ defmodule Pleroma.Export do end end - def bookmarks(dir, %{id: user_id} = _user) do + defp bookmarks(dir, %{id: user_id} = _user) do Bookmark |> where(user_id: ^user_id) |> join(:inner, [b], activity in assoc(b, :activity)) @@ -90,7 +90,7 @@ defmodule Pleroma.Export do |> write(dir, "bookmarks", fn a -> {:ok, "\"#{a.object}\""} end) end - def likes(dir, user) do + defp likes(dir, user) do user.ap_id |> Activity.Queries.by_actor() |> Activity.Queries.by_type("Like") @@ -98,7 +98,7 @@ defmodule Pleroma.Export do |> write(dir, "likes", fn a -> {:ok, "\"#{a.object}\""} end) end - def statuses(dir, user) do + defp statuses(dir, user) do opts = %{} |> Map.put(:type, ["Create", "Announce"]) -- cgit v1.2.3 From be42ab70dc9538df54ac6f30ee123666223b7287 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 31 Aug 2020 20:31:21 +0400 Subject: Add backup upload --- lib/pleroma/export.ex | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/export.ex b/lib/pleroma/export.ex index 45b8ce749..b84eccd78 100644 --- a/lib/pleroma/export.ex +++ b/lib/pleroma/export.ex @@ -22,7 +22,25 @@ defmodule Pleroma.Export do :ok <- bookmarks(path, user), {:ok, zip_path} <- :zip.create('#{path}.zip', @files, cwd: path), {:ok, _} <- File.rm_rf(path) do - {:ok, zip_path} + {:ok, :binary.list_to_bin(zip_path)} + end + end + + def upload(zip_path) do + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + file_name = zip_path |> String.split("/") |> List.last() + id = Ecto.UUID.generate() + + upload = %Pleroma.Upload{ + id: id, + name: file_name, + tempfile: zip_path, + content_type: "application/zip", + path: id <> "/" <> file_name + } + + with :ok <- uploader.put_file(upload), :ok <- File.rm(zip_path) do + {:ok, upload} end end -- cgit v1.2.3 From 75e07ba206b94155c5210151a49e29a11bce6e50 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 31 Aug 2020 23:07:14 +0400 Subject: Fix tests --- lib/pleroma/export.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/export.ex b/lib/pleroma/export.ex index b84eccd78..8b1bfefe2 100644 --- a/lib/pleroma/export.ex +++ b/lib/pleroma/export.ex @@ -39,7 +39,8 @@ defmodule Pleroma.Export do path: id <> "/" <> file_name } - with :ok <- uploader.put_file(upload), :ok <- File.rm(zip_path) do + with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload), + :ok <- File.rm(zip_path) do {:ok, upload} end end -- cgit v1.2.3 From 4f3a6337454807f4145bbc1830c3d55dd883d46d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 2 Sep 2020 20:21:33 +0400 Subject: Add `backups` table --- lib/pleroma/backup.ex | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ lib/pleroma/export.ex | 139 ---------------------------------- 2 files changed, 201 insertions(+), 139 deletions(-) create mode 100644 lib/pleroma/backup.ex delete mode 100644 lib/pleroma/export.ex (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex new file mode 100644 index 000000000..4580d8f92 --- /dev/null +++ b/lib/pleroma/backup.ex @@ -0,0 +1,201 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Backup do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.UserView + + schema "backups" do + field(:content_type, :string) + field(:file_name, :string) + field(:file_size, :integer, default: 0) + field(:processed, :boolean, default: false) + + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + + timestamps() + end + + def create(user) do + with :ok <- validate_limit(user), + {:ok, backup} <- user |> new() |> Repo.insert() do + {:ok, backup} + end + end + + def new(user) do + rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now()) + name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip" + + %__MODULE__{ + user_id: user.id, + content_type: "application/zip", + file_name: name + } + end + + defp validate_limit(user) do + case get_last(user.id) do + %__MODULE__{inserted_at: inserted_at} -> + days = 7 + diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days) + + if diff > days do + :ok + else + {:error, "Last export was less than #{days} days ago"} + end + + nil -> + :ok + end + end + + def get_last(user_id) do + __MODULE__ + |> where(user_id: ^user_id) + |> order_by(desc: :id) + |> limit(1) + |> Repo.one() + end + + def process(%__MODULE__{} = backup) do + with {:ok, zip_file} <- zip(backup), + {:ok, %{size: size}} <- File.stat(zip_file), + {:ok, _upload} <- upload(backup, zip_file) do + backup + |> cast(%{file_size: size, processed: true}, [:file_size, :processed]) + |> Repo.update() + end + end + + @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] + def zip(%__MODULE__{} = backup) do + backup = Repo.preload(backup, :user) + name = String.trim_trailing(backup.file_name, ".zip") + dir = Path.join(System.tmp_dir!(), name) + + with :ok <- File.mkdir(dir), + :ok <- actor(dir, backup.user), + :ok <- statuses(dir, backup.user), + :ok <- likes(dir, backup.user), + :ok <- bookmarks(dir, backup.user), + {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir), + {:ok, _} <- File.rm_rf(dir) do + {:ok, :binary.list_to_bin(zip_path)} + end + end + + def upload(%__MODULE__{} = backup, zip_path) do + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + + upload = %Pleroma.Upload{ + name: backup.file_name, + tempfile: zip_path, + content_type: backup.content_type, + path: "backups/" <> backup.file_name + } + + with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload), + :ok <- File.rm(zip_path) do + {:ok, upload} + end + end + + defp actor(dir, user) do + with {:ok, json} <- + UserView.render("user.json", %{user: user}) + |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) + |> Jason.encode() do + File.write(dir <> "/actor.json", json) + end + end + + defp write_header(file, name) do + IO.write( + file, + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "#{name}.json", + "type": "OrderedCollection", + "orderedItems": [ + """ + ) + end + + defp write(query, dir, name, fun) do + path = dir <> "/#{name}.json" + + with {:ok, file} <- File.open(path, [:write, :utf8]), + :ok <- write_header(file, name) do + counter = :counters.new(1, []) + + query + |> Pleroma.RepoStreamer.chunk_stream(100) + |> Stream.each(fn items -> + Enum.each(items, fn i -> + with {:ok, str} <- fun.(i), + :ok <- IO.write(file, str <> ",\n") do + :counters.add(counter, 1, 1) + end + end) + end) + |> Stream.run() + + total = :counters.get(counter, 1) + + with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do + File.close(file) + end + end + end + + defp bookmarks(dir, %{id: user_id} = _user) do + Bookmark + |> where(user_id: ^user_id) + |> join(:inner, [b], activity in assoc(b, :activity)) + |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) + |> write(dir, "bookmarks", fn a -> {:ok, "\"#{a.object}\""} end) + end + + defp likes(dir, user) do + user.ap_id + |> Activity.Queries.by_actor() + |> Activity.Queries.by_type("Like") + |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) + |> write(dir, "likes", fn a -> {:ok, "\"#{a.object}\""} end) + end + + defp statuses(dir, user) do + opts = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) + + [[user.ap_id], User.following(user), Pleroma.List.memberships(user)] + |> Enum.concat() + |> ActivityPub.fetch_activities_query(opts) + |> write(dir, "outbox", fn a -> + with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do + activity |> Map.delete("@context") |> Jason.encode() + end + end) + end +end diff --git a/lib/pleroma/export.ex b/lib/pleroma/export.ex deleted file mode 100644 index 8b1bfefe2..000000000 --- a/lib/pleroma/export.ex +++ /dev/null @@ -1,139 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Export do - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.UserView - - import Ecto.Query - - @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] - - def run(user) do - with {:ok, path} <- create_dir(user), - :ok <- actor(path, user), - :ok <- statuses(path, user), - :ok <- likes(path, user), - :ok <- bookmarks(path, user), - {:ok, zip_path} <- :zip.create('#{path}.zip', @files, cwd: path), - {:ok, _} <- File.rm_rf(path) do - {:ok, :binary.list_to_bin(zip_path)} - end - end - - def upload(zip_path) do - uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) - file_name = zip_path |> String.split("/") |> List.last() - id = Ecto.UUID.generate() - - upload = %Pleroma.Upload{ - id: id, - name: file_name, - tempfile: zip_path, - content_type: "application/zip", - path: id <> "/" <> file_name - } - - with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload), - :ok <- File.rm(zip_path) do - {:ok, upload} - end - end - - defp actor(dir, user) do - with {:ok, json} <- - UserView.render("user.json", %{user: user}) - |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) - |> Jason.encode() do - File.write(dir <> "/actor.json", json) - end - end - - defp create_dir(user) do - datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now()) - dir = Path.join(System.tmp_dir!(), "archive-#{user.id}-#{datetime}") - - with :ok <- File.mkdir(dir), do: {:ok, dir} - end - - defp write_header(file, name) do - IO.write( - file, - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "#{name}.json", - "type": "OrderedCollection", - "orderedItems": [ - """ - ) - end - - defp write(query, dir, name, fun) do - path = dir <> "/#{name}.json" - - with {:ok, file} <- File.open(path, [:write, :utf8]), - :ok <- write_header(file, name) do - counter = :counters.new(1, []) - - query - |> Pleroma.RepoStreamer.chunk_stream(100) - |> Stream.each(fn items -> - Enum.each(items, fn i -> - with {:ok, str} <- fun.(i), - :ok <- IO.write(file, str <> ",\n") do - :counters.add(counter, 1, 1) - end - end) - end) - |> Stream.run() - - total = :counters.get(counter, 1) - - with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do - File.close(file) - end - end - end - - defp bookmarks(dir, %{id: user_id} = _user) do - Bookmark - |> where(user_id: ^user_id) - |> join(:inner, [b], activity in assoc(b, :activity)) - |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) - |> write(dir, "bookmarks", fn a -> {:ok, "\"#{a.object}\""} end) - end - - defp likes(dir, user) do - user.ap_id - |> Activity.Queries.by_actor() - |> Activity.Queries.by_type("Like") - |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) - |> write(dir, "likes", fn a -> {:ok, "\"#{a.object}\""} end) - end - - defp statuses(dir, user) do - opts = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> Map.put(:announce_filtering_user, user) - |> Map.put(:user, user) - - [[user.ap_id], User.following(user), Pleroma.List.memberships(user)] - |> Enum.concat() - |> ActivityPub.fetch_activities_query(opts) - |> write(dir, "outbox", fn a -> - with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do - activity |> Map.delete("@context") |> Jason.encode() - end - end) - end -end -- cgit v1.2.3 From a0ad9bd734e9af0ce912c32c7480a60ff87a4368 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 2 Sep 2020 21:45:22 +0400 Subject: Add BackupWorker --- lib/pleroma/backup.ex | 11 ++++++++++- lib/pleroma/workers/backup_worker.ex | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/workers/backup_worker.ex (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 4580d8f92..9b5d2625f 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Backup do def create(user) do with :ok <- validate_limit(user), {:ok, backup} <- user |> new() |> Repo.insert() do - {:ok, backup} + Pleroma.Workers.BackupWorker.enqueue("process", %{"backup_id" => backup.id}) end end @@ -71,6 +71,15 @@ defmodule Pleroma.Backup do |> Repo.one() end + def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do + __MODULE__ + |> where(user_id: ^user_id) + |> where([b], b.id != ^latest_id) + |> Repo.delete_all() + end + + def get(id), do: Repo.get(__MODULE__, id) + def process(%__MODULE__{} = backup) do with {:ok, zip_file} <- zip(backup), {:ok, %{size: size}} <- File.stat(zip_file), diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex new file mode 100644 index 000000000..c982ffa3a --- /dev/null +++ b/lib/pleroma/workers/backup_worker.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.BackupWorker do + alias Pleroma.Backup + + use Pleroma.Workers.WorkerHelper, queue: "backup" + + @impl Oban.Worker + def perform(%Job{args: %{"op" => "process", "backup_id" => backup_id}}) do + with {:ok, %Backup{} = backup} <- + backup_id |> Backup.get() |> Backup.process() do + {:ok, backup} + end + end +end -- cgit v1.2.3 From 3ad7492f9dd1c76cdbc64ad2246f8e9c8c5c4ae6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 4 Sep 2020 18:30:39 +0400 Subject: Add config for Pleroma.Backup --- lib/pleroma/backup.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 9b5d2625f..e384b6b00 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -49,7 +49,7 @@ defmodule Pleroma.Backup do defp validate_limit(user) do case get_last(user.id) do %__MODULE__{inserted_at: inserted_at} -> - days = 7 + days = Pleroma.Config.get([Pleroma.Backup, :limit_days]) diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days) if diff > days do -- cgit v1.2.3 From 739cb1463ba07513f047b2ac8f7e22a16c89ef4e Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 4 Sep 2020 21:48:52 +0400 Subject: Add backups deletion --- lib/pleroma/backup.ex | 14 ++++++++++++-- lib/pleroma/workers/backup_worker.ex | 37 +++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index e384b6b00..bd50fd910 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Backup do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Workers.BackupWorker schema "backups" do field(:content_type, :string) @@ -30,7 +31,7 @@ defmodule Pleroma.Backup do def create(user) do with :ok <- validate_limit(user), {:ok, backup} <- user |> new() |> Repo.insert() do - Pleroma.Workers.BackupWorker.enqueue("process", %{"backup_id" => backup.id}) + BackupWorker.process(backup) end end @@ -46,6 +47,14 @@ defmodule Pleroma.Backup do } end + def delete(backup) do + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + + with :ok <- uploader.delete_file("backups/" <> backup.file_name) do + Repo.delete(backup) + end + end + defp validate_limit(user) do case get_last(user.id) do %__MODULE__{inserted_at: inserted_at} -> @@ -75,7 +84,8 @@ defmodule Pleroma.Backup do __MODULE__ |> where(user_id: ^user_id) |> where([b], b.id != ^latest_id) - |> Repo.delete_all() + |> Repo.all() + |> Enum.each(&BackupWorker.delete/1) end def get(id), do: Repo.get(__MODULE__, id) diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex index c982ffa3a..f40020794 100644 --- a/lib/pleroma/workers/backup_worker.ex +++ b/lib/pleroma/workers/backup_worker.ex @@ -3,15 +3,46 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.BackupWorker do + use Oban.Worker, queue: :backup, max_attempts: 1 + + alias Oban.Job alias Pleroma.Backup - use Pleroma.Workers.WorkerHelper, queue: "backup" + def process(backup) do + %{"op" => "process", "backup_id" => backup.id} + |> new() + |> Oban.insert() + end + + def schedule_deletion(backup) do + days = Pleroma.Config.get([Pleroma.Backup, :purge_after_days]) + time = 60 * 60 * 24 * days + scheduled_at = Calendar.NaiveDateTime.add!(backup.inserted_at, time) + + %{"op" => "delete", "backup_id" => backup.id} + |> new(scheduled_at: scheduled_at) + |> Oban.insert() + end + + def delete(backup) do + %{"op" => "delete", "backup_id" => backup.id} + |> new() + |> Oban.insert() + end - @impl Oban.Worker def perform(%Job{args: %{"op" => "process", "backup_id" => backup_id}}) do with {:ok, %Backup{} = backup} <- - backup_id |> Backup.get() |> Backup.process() do + backup_id |> Backup.get() |> Backup.process(), + {:ok, _job} <- schedule_deletion(backup), + :ok <- Backup.remove_outdated(backup) do {:ok, backup} end end + + def perform(%Job{args: %{"op" => "delete", "backup_id" => backup_id}}) do + case Backup.get(backup_id) do + %Backup{} = backup -> Backup.delete(backup) + nil -> :ok + end + end end -- cgit v1.2.3 From 2c73bfe1227065fa203b0b78c9eb12cf86ab3948 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 9 Sep 2020 01:04:00 +0400 Subject: Add API endpoints for Backups --- lib/pleroma/backup.ex | 7 ++ .../operations/pleroma_backup_operation.ex | 79 ++++++++++++++++++++++ .../pleroma_api/controllers/backup_controller.ex | 27 ++++++++ lib/pleroma/web/pleroma_api/views/backup_view.ex | 24 +++++++ lib/pleroma/web/router.ex | 3 + 5 files changed, 140 insertions(+) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/backup_controller.ex create mode 100644 lib/pleroma/web/pleroma_api/views/backup_view.ex (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index bd50fd910..348e537a8 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -80,6 +80,13 @@ defmodule Pleroma.Backup do |> Repo.one() end + def list(%User{id: user_id}) do + __MODULE__ + |> where(user_id: ^user_id) + |> order_by(desc: :id) + |> Repo.all() + end + def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do __MODULE__ |> where(user_id: ^user_id) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex new file mode 100644 index 000000000..f877ca31b --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Backups"], + summary: "List backups", + security: [%{"oAuth" => ["read:account"]}], + operationId: "PleromaAPI.BackupController.index", + responses: %{ + 200 => + Operation.response( + "An array of backups", + "application/json", + %Schema{ + type: :array, + items: backup() + } + ), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def create_operation do + %Operation{ + tags: ["Backups"], + summary: "Create a backup", + security: [%{"oAuth" => ["read:account"]}], + operationId: "PleromaAPI.BackupController.create", + responses: %{ + 200 => + Operation.response( + "An array of backups", + "application/json", + %Schema{ + type: :array, + items: backup() + } + ), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + defp backup do + %Schema{ + title: "Backup", + description: "Response schema for a backup", + type: :object, + properties: %{ + inserted_at: %Schema{type: :string, format: :"date-time"}, + content_type: %Schema{type: :string}, + file_name: %Schema{type: :string}, + file_size: %Schema{type: :integer}, + processed: %Schema{type: :boolean} + }, + example: %{ + "content_type" => "application/zip", + "file_name" => + "archive-cofe-20200908T195819-1lWrJyJqpsj8-KuHFr7N03lfsYYa5nf2NL-7A9-ddFU.zip", + "file_size" => 1024, + "inserted_at" => "2020-09-08T19:58:20", + "processed" => true + } + } + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex new file mode 100644 index 000000000..e52c77ff2 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.BackupController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.OAuthScopesPlug + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [:index, :create]) + plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation + + def index(%{assigns: %{user: user}} = conn, _params) do + backups = Pleroma.Backup.list(user) + render(conn, "index.json", backups: backups) + end + + def create(%{assigns: %{user: user}} = conn, _params) do + with {:ok, _} <- Pleroma.Backup.create(user) do + backups = Pleroma.Backup.list(user) + render(conn, "index.json", backups: backups) + end + end +end diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex new file mode 100644 index 000000000..02b94ce4f --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/backup_view.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.BackupView do + use Pleroma.Web, :view + + alias Pleroma.Backup + alias Pleroma.Web.CommonAPI.Utils + + def render("show.json", %{backup: %Backup{} = backup}) do + %{ + content_type: backup.content_type, + file_name: backup.file_name, + file_size: backup.file_size, + processed: backup.processed, + inserted_at: Utils.to_masto_date(backup.inserted_at) + } + end + + def render("index.json", %{backups: backups}) do + render_many(backups, __MODULE__, "show.json") + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e22b31b4c..a1a5a1cb5 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -293,6 +293,9 @@ defmodule Pleroma.Web.Router do get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup) post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm) delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable) + + get("/backups", BackupController, :index) + post("/backups", BackupController, :create) end scope "/oauth", Pleroma.Web.OAuth do -- cgit v1.2.3 From 86ce4afd9338d81f741fa57f962509a6f0f50aff Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 9 Sep 2020 20:02:20 +0400 Subject: Improve backup urls --- lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex | 6 +++--- lib/pleroma/web/pleroma_api/views/backup_view.ex | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex index f877ca31b..6993794db 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_backup_operation.ex @@ -69,9 +69,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do example: %{ "content_type" => "application/zip", "file_name" => - "archive-cofe-20200908T195819-1lWrJyJqpsj8-KuHFr7N03lfsYYa5nf2NL-7A9-ddFU.zip", - "file_size" => 1024, - "inserted_at" => "2020-09-08T19:58:20", + "https://cofe.fe:4000/media/backups/archive-foobar-20200908T164207-Yr7vuT5Wycv-sN3kSN2iJ0k-9pMo60j9qmvRCdDqIew.zip", + "file_size" => 4105, + "inserted_at" => "2020-09-08T16:42:07.000Z", "processed" => true } } diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex index 02b94ce4f..bf40a001e 100644 --- a/lib/pleroma/web/pleroma_api/views/backup_view.ex +++ b/lib/pleroma/web/pleroma_api/views/backup_view.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do def render("show.json", %{backup: %Backup{} = backup}) do %{ content_type: backup.content_type, - file_name: backup.file_name, + url: download_url(backup), file_size: backup.file_size, processed: backup.processed, inserted_at: Utils.to_masto_date(backup.inserted_at) @@ -21,4 +21,8 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do def render("index.json", %{backups: backups}) do render_many(backups, __MODULE__, "show.json") end + + def download_url(%Backup{file_name: file_name}) do + Pleroma.Web.Endpoint.url() <> "/media/backups/" <> file_name + end end -- cgit v1.2.3 From cd13613db3f675b6a9171dea56fc5b03e43ae6b0 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 10 Sep 2020 20:53:06 +0400 Subject: Fix query --- lib/pleroma/backup.ex | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 348e537a8..ce54a413a 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -8,6 +8,8 @@ defmodule Pleroma.Backup do import Ecto.Changeset import Ecto.Query + require Pleroma.Constants + alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.Repo @@ -158,6 +160,7 @@ defmodule Pleroma.Backup do "id": "#{name}.json", "type": "OrderedCollection", "orderedItems": [ + """ ) end @@ -209,13 +212,13 @@ defmodule Pleroma.Backup do opts = %{} |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:reply_filtering_user, user) - |> Map.put(:announce_filtering_user, user) - |> Map.put(:user, user) + |> Map.put(:actor_id, user.ap_id) - [[user.ap_id], User.following(user), Pleroma.List.memberships(user)] + [ + [Pleroma.Constants.as_public(), user.ap_id], + User.following(user), + Pleroma.List.memberships(user) + ] |> Enum.concat() |> ActivityPub.fetch_activities_query(opts) |> write(dir, "outbox", fn a -> -- cgit v1.2.3 From 27bc121ec00a7b088030d6fb36c7e731f5b072b6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 15 Sep 2020 18:07:28 +0400 Subject: Require email --- lib/pleroma/backup.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index ce54a413a..3b85dd1c1 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -31,7 +31,9 @@ defmodule Pleroma.Backup do end def create(user) do - with :ok <- validate_limit(user), + with :ok <- validate_email_enabled(), + :ok <- validate_user_email(user), + :ok <- validate_limit(user), {:ok, backup} <- user |> new() |> Repo.insert() do BackupWorker.process(backup) end @@ -74,6 +76,17 @@ defmodule Pleroma.Backup do end end + defp validate_email_enabled do + if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do + :ok + else + {:error, "Backups require enabled email"} + end + end + + defp validate_user_email(%User{email: nil}), do: {:error, "Email is required"} + defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok + def get_last(user_id) do __MODULE__ |> where(user_id: ^user_id) @@ -100,7 +113,7 @@ defmodule Pleroma.Backup do def get(id), do: Repo.get(__MODULE__, id) def process(%__MODULE__{} = backup) do - with {:ok, zip_file} <- zip(backup), + with {:ok, zip_file} <- export(backup), {:ok, %{size: size}} <- File.stat(zip_file), {:ok, _upload} <- upload(backup, zip_file) do backup @@ -110,7 +123,7 @@ defmodule Pleroma.Backup do end @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] - def zip(%__MODULE__{} = backup) do + def export(%__MODULE__{} = backup) do backup = Repo.preload(backup, :user) name = String.trim_trailing(backup.file_name, ".zip") dir = Path.join(System.tmp_dir!(), name) -- cgit v1.2.3 From e52dd62e14a956a28a706124464f3ac4b985080d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 16 Sep 2020 23:21:13 +0400 Subject: Add configurable temporary directory --- lib/pleroma/backup.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 3b85dd1c1..450dd5b84 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -126,7 +126,7 @@ defmodule Pleroma.Backup do def export(%__MODULE__{} = backup) do backup = Repo.preload(backup, :user) name = String.trim_trailing(backup.file_name, ".zip") - dir = Path.join(System.tmp_dir!(), name) + dir = dir(name) with :ok <- File.mkdir(dir), :ok <- actor(dir, backup.user), @@ -139,6 +139,11 @@ defmodule Pleroma.Backup do end end + def dir(name) do + dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!() + Path.join(dir, name) + end + def upload(%__MODULE__{} = backup, zip_path) do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) -- cgit v1.2.3 From 7fdd81d000d857cbcd5bf442f68c91b1c5b1cebb Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 17 Sep 2020 18:42:24 +0400 Subject: Add "Your backup is ready" email --- lib/pleroma/emails/user_email.ex | 16 ++++++++++++++++ lib/pleroma/workers/backup_worker.ex | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 1d8c72ae9..f943dda0d 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -189,4 +189,20 @@ defmodule Pleroma.Emails.UserEmail do Router.Helpers.subscription_url(Endpoint, :unsubscribe, token) end + + def backup_is_ready_email(backup) do + %{user: user} = Pleroma.Repo.preload(backup, :user) + download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup) + + html_body = """ +

You requested a full backup of your Pleroma account. It's ready for download:

+

+ """ + + new() + |> to(recipient(user)) + |> from(sender()) + |> subject("Your account archive is ready") + |> html_body(html_body) + end end diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex index f40020794..405d55269 100644 --- a/lib/pleroma/workers/backup_worker.ex +++ b/lib/pleroma/workers/backup_worker.ex @@ -34,7 +34,11 @@ defmodule Pleroma.Workers.BackupWorker do with {:ok, %Backup{} = backup} <- backup_id |> Backup.get() |> Backup.process(), {:ok, _job} <- schedule_deletion(backup), - :ok <- Backup.remove_outdated(backup) do + :ok <- Backup.remove_outdated(backup), + {:ok, _} <- + backup + |> Pleroma.Emails.UserEmail.backup_is_ready_email() + |> Pleroma.Emails.Mailer.deliver() do {:ok, backup} end end -- cgit v1.2.3 From 7c22c9afb410668d87dcd4a90651d62d9a1e9e4d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 18 Sep 2020 22:18:34 +0400 Subject: Allow admins request user backups --- lib/pleroma/backup.ex | 4 ++-- lib/pleroma/emails/user_email.ex | 20 +++++++++++++++----- .../admin_api/controllers/admin_api_controller.ex | 12 +++++++++++- lib/pleroma/web/router.ex | 2 ++ lib/pleroma/workers/backup_worker.ex | 10 ++++++---- 5 files changed, 36 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 450dd5b84..d589f12f1 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -30,12 +30,12 @@ defmodule Pleroma.Backup do timestamps() end - def create(user) do + def create(user, admin_user_id \\ nil) do with :ok <- validate_email_enabled(), :ok <- validate_user_email(user), :ok <- validate_limit(user), {:ok, backup} <- user |> new() |> Repo.insert() do - BackupWorker.process(backup) + BackupWorker.process(backup, admin_user_id) end end diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index f943dda0d..5745794ec 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -190,14 +190,24 @@ defmodule Pleroma.Emails.UserEmail do Router.Helpers.subscription_url(Endpoint, :unsubscribe, token) end - def backup_is_ready_email(backup) do + def backup_is_ready_email(backup, admin_user_id \\ nil) do %{user: user} = Pleroma.Repo.preload(backup, :user) download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup) - html_body = """ -

You requested a full backup of your Pleroma account. It's ready for download:

-

- """ + html_body = + if is_nil(admin_user_id) do + """ +

You requested a full backup of your Pleroma account. It's ready for download:

+

+ """ + else + admin = Pleroma.Repo.get(User, admin_user_id) + + """ +

Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:

+

+ """ + end new() |> to(recipient(user)) diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index d5713c3dd..f7d2fe5b1 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -23,12 +23,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.Endpoint alias Pleroma.Web.Router + require Logger + @users_page_size 50 plug( OAuthScopesPlug, %{scopes: ["read:accounts"], admin: true} - when action in [:list_users, :user_show, :right_get, :show_user_credentials] + when action in [:list_users, :user_show, :right_get, :show_user_credentials, :create_backup] ) plug( @@ -681,6 +683,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do json(conn, %{"status_visibility" => counters}) end + def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + with %User{} = user <- User.get_by_nickname(nickname), + {:ok, _} <- Pleroma.Backup.create(user, admin.id) do + Logger.info("Admin @#{admin.nickname} requested account backup for @{nickname}") + json(conn, "") + end + end + defp page_params(params) do {get_page(params["page"]), get_page_size(params["page_size"])} end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a1a5a1cb5..e539eeeeb 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -129,6 +129,8 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through(:admin_api) + post("/backups", AdminAPIController, :create_backup) + post("/users/follow", AdminAPIController, :user_follow) post("/users/unfollow", AdminAPIController, :user_unfollow) diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex index 405d55269..65754b6a2 100644 --- a/lib/pleroma/workers/backup_worker.ex +++ b/lib/pleroma/workers/backup_worker.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Workers.BackupWorker do alias Oban.Job alias Pleroma.Backup - def process(backup) do - %{"op" => "process", "backup_id" => backup.id} + def process(backup, admin_user_id \\ nil) do + %{"op" => "process", "backup_id" => backup.id, "admin_user_id" => admin_user_id} |> new() |> Oban.insert() end @@ -30,14 +30,16 @@ defmodule Pleroma.Workers.BackupWorker do |> Oban.insert() end - def perform(%Job{args: %{"op" => "process", "backup_id" => backup_id}}) do + def perform(%Job{ + args: %{"op" => "process", "backup_id" => backup_id, "admin_user_id" => admin_user_id} + }) do with {:ok, %Backup{} = backup} <- backup_id |> Backup.get() |> Backup.process(), {:ok, _job} <- schedule_deletion(backup), :ok <- Backup.remove_outdated(backup), {:ok, _} <- backup - |> Pleroma.Emails.UserEmail.backup_is_ready_email() + |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id) |> Pleroma.Emails.Mailer.deliver() do {:ok, backup} end -- cgit v1.2.3 From e50314d9d342dbf9a03ca484654b07717592d4bd Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 18 Sep 2020 22:33:12 +0400 Subject: Fix export --- lib/pleroma/backup.ex | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index d589f12f1..242773bdb 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -191,16 +191,13 @@ defmodule Pleroma.Backup do counter = :counters.new(1, []) query - |> Pleroma.RepoStreamer.chunk_stream(100) - |> Stream.each(fn items -> - Enum.each(items, fn i -> - with {:ok, str} <- fun.(i), - :ok <- IO.write(file, str <> ",\n") do - :counters.add(counter, 1, 1) - end - end) + |> Pleroma.Repo.chunk_stream(100) + |> Enum.each(fn i -> + with {:ok, str} <- fun.(i), + :ok <- IO.write(file, str <> ",\n") do + :counters.add(counter, 1, 1) + end end) - |> Stream.run() total = :counters.get(counter, 1) -- cgit v1.2.3 From a9efd441e242f1d8ac608b866d0cfafe4833243a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sun, 20 Sep 2020 19:57:09 +0400 Subject: Use `Pleroma.Repo.chunk_stream/2` instead of `Pleroma.RepoStreamer.chunk_stream/2` --- lib/pleroma/backup.ex | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 242773bdb..f5f39431d 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -188,18 +188,17 @@ defmodule Pleroma.Backup do with {:ok, file} <- File.open(path, [:write, :utf8]), :ok <- write_header(file, name) do - counter = :counters.new(1, []) - - query - |> Pleroma.Repo.chunk_stream(100) - |> Enum.each(fn i -> - with {:ok, str} <- fun.(i), - :ok <- IO.write(file, str <> ",\n") do - :counters.add(counter, 1, 1) - end - end) - - total = :counters.get(counter, 1) + total = + query + |> Pleroma.Repo.chunk_stream(100) + |> Enum.reduce(0, fn i, acc -> + with {:ok, str} <- fun.(i), + :ok <- IO.write(file, str <> ",\n") do + acc + 1 + else + _ -> acc + end + end) with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do File.close(file) -- cgit v1.2.3 From 17562bf4147ab03e171b1f1d365a512f2e5b3202 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sun, 20 Sep 2020 20:43:27 +0400 Subject: Move API endpoints to `/api/v1/pleroma/backups` --- lib/pleroma/web/router.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e539eeeeb..ad7e315c7 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -295,9 +295,6 @@ defmodule Pleroma.Web.Router do get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup) post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm) delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable) - - get("/backups", BackupController, :index) - post("/backups", BackupController, :create) end scope "/oauth", Pleroma.Web.OAuth do @@ -358,6 +355,9 @@ defmodule Pleroma.Web.Router do put("/mascot", MascotController, :update) post("/scrobble", ScrobbleController, :create) + + get("/backups", BackupController, :index) + post("/backups", BackupController, :create) end scope [] do -- cgit v1.2.3 From e4792ce76af3094d378a3a201ca429ae38203696 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sun, 20 Sep 2020 21:06:16 +0400 Subject: Do not limit admins --- lib/pleroma/backup.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index f5f39431d..e2673db80 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -30,12 +30,12 @@ defmodule Pleroma.Backup do timestamps() end - def create(user, admin_user_id \\ nil) do + def create(user, admin_id \\ nil) do with :ok <- validate_email_enabled(), :ok <- validate_user_email(user), - :ok <- validate_limit(user), + :ok <- validate_limit(user, admin_id), {:ok, backup} <- user |> new() |> Repo.insert() do - BackupWorker.process(backup, admin_user_id) + BackupWorker.process(backup, admin_id) end end @@ -59,7 +59,9 @@ defmodule Pleroma.Backup do end end - defp validate_limit(user) do + defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok + + defp validate_limit(user, nil) do case get_last(user.id) do %__MODULE__{inserted_at: inserted_at} -> days = Pleroma.Config.get([Pleroma.Backup, :limit_days]) -- cgit v1.2.3 From 8baee855d90530def46dc62b81e6a0cb0c315914 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 21 Sep 2020 21:47:36 +0400 Subject: Fix emails --- lib/pleroma/emails/user_email.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 5745794ec..806a61fd2 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -198,14 +198,14 @@ defmodule Pleroma.Emails.UserEmail do if is_nil(admin_user_id) do """

You requested a full backup of your Pleroma account. It's ready for download:

-

+

#{download_url}

""" else admin = Pleroma.Repo.get(User, admin_user_id) """

Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:

-

+

#{download_url}

""" end -- cgit v1.2.3 From d7a5291b4fa3b7568674c0f7643fe287fcd21eff Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sat, 26 Sep 2020 21:24:35 +0400 Subject: Use `Jason.encode/1` for likes and bookmarks --- lib/pleroma/backup.ex | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index e2673db80..b43dc94d6 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -194,7 +194,8 @@ defmodule Pleroma.Backup do query |> Pleroma.Repo.chunk_stream(100) |> Enum.reduce(0, fn i, acc -> - with {:ok, str} <- fun.(i), + with {:ok, data} <- fun.(i), + {:ok, str} <- Jason.encode(data), :ok <- IO.write(file, str <> ",\n") do acc + 1 else @@ -213,7 +214,7 @@ defmodule Pleroma.Backup do |> where(user_id: ^user_id) |> join(:inner, [b], activity in assoc(b, :activity)) |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) - |> write(dir, "bookmarks", fn a -> {:ok, "\"#{a.object}\""} end) + |> write(dir, "bookmarks", fn a -> {:ok, a.object} end) end defp likes(dir, user) do @@ -221,7 +222,7 @@ defmodule Pleroma.Backup do |> Activity.Queries.by_actor() |> Activity.Queries.by_type("Like") |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) - |> write(dir, "likes", fn a -> {:ok, "\"#{a.object}\""} end) + |> write(dir, "likes", fn a -> {:ok, a.object} end) end defp statuses(dir, user) do @@ -239,7 +240,7 @@ defmodule Pleroma.Backup do |> ActivityPub.fetch_activities_query(opts) |> write(dir, "outbox", fn a -> with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do - activity |> Map.delete("@context") |> Jason.encode() + {:ok, Map.delete(activity, "@context")} end end) end -- cgit v1.2.3 From 9af9f02f4b3c4eac859a69ab9b2f546a91110287 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sat, 26 Sep 2020 21:45:03 +0400 Subject: Use Gettext for error messages --- lib/pleroma/backup.ex | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index b43dc94d6..0ebaf02e5 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Backup do import Ecto.Changeset import Ecto.Query + import Pleroma.Web.Gettext require Pleroma.Constants @@ -70,7 +71,14 @@ defmodule Pleroma.Backup do if diff > days do :ok else - {:error, "Last export was less than #{days} days ago"} + {:error, + dngettext( + "errors", + "Last export was less than a day ago", + "Last export was less than %{days} days ago", + days, + days: days + )} end nil -> @@ -82,11 +90,14 @@ defmodule Pleroma.Backup do if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do :ok else - {:error, "Backups require enabled email"} + {:error, dgettext("errors", "Backups require enabled email")} end end - defp validate_user_email(%User{email: nil}), do: {:error, "Email is required"} + defp validate_user_email(%User{email: nil}) do + {:error, dgettext("errors", "Email is required")} + end + defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok def get_last(user_id) do -- cgit v1.2.3 From 08972dd135c200073f5de0c8731b886cc2e72eeb Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sat, 26 Sep 2020 21:50:31 +0400 Subject: Use Path.join/2 --- lib/pleroma/backup.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index 0ebaf02e5..cee51d7c1 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -55,7 +55,7 @@ defmodule Pleroma.Backup do def delete(backup) do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) - with :ok <- uploader.delete_file("backups/" <> backup.file_name) do + with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do Repo.delete(backup) end end @@ -164,7 +164,7 @@ defmodule Pleroma.Backup do name: backup.file_name, tempfile: zip_path, content_type: backup.content_type, - path: "backups/" <> backup.file_name + path: Path.join("backups", backup.file_name) } with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload), @@ -178,7 +178,7 @@ defmodule Pleroma.Backup do UserView.render("user.json", %{user: user}) |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) |> Jason.encode() do - File.write(dir <> "/actor.json", json) + File.write(Path.join(dir, "actor.json"), json) end end @@ -197,7 +197,7 @@ defmodule Pleroma.Backup do end defp write(query, dir, name, fun) do - path = dir <> "/#{name}.json" + path = Path.join(dir, "#{name}.json") with {:ok, file} <- File.open(path, [:write, :utf8]), :ok <- write_header(file, name) do -- cgit v1.2.3 From 8545d533ddee2978e9bf7f3284cc7dcb822a77e6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sat, 26 Sep 2020 21:53:04 +0400 Subject: Use to_string/1 instead of :binary.list_to_bin/1 --- lib/pleroma/backup.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex index cee51d7c1..629e879a7 100644 --- a/lib/pleroma/backup.ex +++ b/lib/pleroma/backup.ex @@ -148,7 +148,7 @@ defmodule Pleroma.Backup do :ok <- bookmarks(dir, backup.user), {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir), {:ok, _} <- File.rm_rf(dir) do - {:ok, :binary.list_to_bin(zip_path)} + {:ok, to_string(zip_path)} end end -- cgit v1.2.3 From bc3db724030707e9903d161a70b10fe217a83212 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sat, 26 Sep 2020 23:16:56 +0400 Subject: Use ModerationLog instead of Logger --- lib/pleroma/moderation_log.ex | 10 ++++++++++ lib/pleroma/web/admin_api/controllers/admin_api_controller.ex | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 47036a6f6..be1e81467 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -651,6 +651,16 @@ defmodule Pleroma.ModerationLog do "@#{actor_nickname} deleted chat message ##{subject_id}" end + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "create_backup", + "subject" => %{"nickname" => user_nickname} + } + }) do + "@#{actor_nickname} requested account backup for @#{user_nickname}" + end + defp nicknames_to_string(nicknames) do nicknames |> Enum.map(&"@#{&1}") diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index f7d2fe5b1..8b5310d80 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -686,7 +686,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_by_nickname(nickname), {:ok, _} <- Pleroma.Backup.create(user, admin.id) do - Logger.info("Admin @#{admin.nickname} requested account backup for @{nickname}") + ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"}) + json(conn, "") end end -- cgit v1.2.3 From 636c00037d797161c4ecd654345a436452f99415 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 09:58:45 -0500 Subject: Fix duplicate @doc entries --- lib/pleroma/web/activity_pub/publisher.ex | 4 +--- lib/pleroma/web/common_api/utils.ex | 11 +---------- lib/pleroma/web/mastodon_api/controllers/auth_controller.ex | 4 ++-- lib/pleroma/web/mastodon_api/controllers/status_controller.ex | 8 ++------ 4 files changed, 6 insertions(+), 21 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 9c3956683..a2930c1cd 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -242,9 +242,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do end) end - @doc """ - Publishes an activity to all relevant peers. - """ + # Publishes an activity to all relevant peers. def publish(%User{} = actor, %Activity{} = activity) do public = is_public?(activity) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 9d7b24eb2..85dcd89dc 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -274,7 +274,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do def format_input(text, format, options \\ []) @doc """ - Formatting text to plain text. + Formatting text to plain text, BBCode, HTML, or Markdown """ def format_input(text, "text/plain", options) do text @@ -285,9 +285,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do end).() end - @doc """ - Formatting text as BBCode. - """ def format_input(text, "text/bbcode", options) do text |> String.replace(~r/\r/, "") @@ -297,18 +294,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Formatter.linkify(options) end - @doc """ - Formatting text to html. - """ def format_input(text, "text/html", options) do text |> Formatter.html_escape("text/html") |> Formatter.linkify(options) end - @doc """ - Formatting text to markdown. - """ def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index 57c0be5fe..a278ca622 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do redirect(conn, to: local_mastodon_root_path(conn)) end - @doc "Local Mastodon FE login init action" + # Local Mastodon FE login init action def login(conn, %{"code" => auth_token}) do with {:ok, app} <- get_or_make_app(), {:ok, auth} <- Authorization.get_by_token(app, auth_token), @@ -35,7 +35,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do end end - @doc "Local Mastodon FE callback action" + # Local Mastodon FE callback action def login(conn, _) do with {:ok, app} <- get_or_make_app() do path = diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index ecfa38489..c85757f26 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -123,9 +123,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc """ POST /api/v1/statuses - - Creates a scheduled status when `scheduled_at` param is present and it's far enough """ + # Creates a scheduled status when `scheduled_at` param is present and it's far enough def create( %{ assigns: %{user: user}, @@ -156,11 +155,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end end - @doc """ - POST /api/v1/statuses - Creates a regular status - """ + # Creates a regular status def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) -- cgit v1.2.3 From d3106c69c80af58244faa0373b01c618371f84e0 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 10:02:20 -0500 Subject: Fix incompatible type (Elixir 1.11) --- lib/pleroma/web/media_proxy/invalidations/http.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex index bb81d8888..694eb559b 100644 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do {:ok, %{status: status} = env} when 400 <= status and status < 500 -> {:error, env} - {:error, error} = error -> + {:error, error} -> error _ -> -- cgit v1.2.3 From 218a3e61e1692058aaf5f16506cbf276d2260722 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 10:04:25 -0500 Subject: Fix incompatible types warning (Elixir 1.11) --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 732c44271..8916aba5f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -412,7 +412,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do object = object |> Map.merge(Map.take(params, ["to", "cc"])) - |> Map.put("attributedTo", user.ap_id()) + |> Map.put("attributedTo", user.ap_id) |> Transmogrifier.fix_object() ActivityPub.create(%{ @@ -456,7 +456,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do %{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{"nickname" => nickname} = params ) do - actor = user.ap_id() + actor = user.ap_id params = params -- cgit v1.2.3 From 6d1666a080ad97ba1233d50ba36d3b8a136f75a7 Mon Sep 17 00:00:00 2001 From: feld Date: Wed, 7 Oct 2020 16:44:52 +0000 Subject: Apply 1 suggestion(s) to 1 file(s) --- lib/pleroma/web/media_proxy/invalidations/http.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex index 694eb559b..0b0cde68c 100644 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do {:ok, %{status: status} = env} when 400 <= status and status < 500 -> {:error, env} - {:error, error} -> + {:error, _} = error -> error _ -> -- cgit v1.2.3 From 8caa6cf91d0d354ce64c1d923112d358295222a2 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 11:47:10 -0500 Subject: Transport.connect/7 is deprecated --- lib/transports.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/transports.ex b/lib/transports.ex index aab7fad99..1ed3a942d 100644 --- a/lib/transports.ex +++ b/lib/transports.ex @@ -31,7 +31,12 @@ defmodule Phoenix.Transports.WebSocket.Raw do case conn do %{halted: false} = conn -> - case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do + case Transport.connect(%{ + endpoint: endpoint, + transport: transport, + options: [serializer: nil], + params: conn.params + }) do {:ok, socket} -> {:ok, conn, {__MODULE__, {socket, opts}}} -- cgit v1.2.3 From 42e78a08b28f3faee21b803c1e621e96bbf5c731 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 12:30:55 -0500 Subject: Fix rendering of reports --- lib/pleroma/web/admin_api/views/report_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 773f798fe..535556370 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do end def render("index_notes.json", %{notes: notes}) when is_list(notes) do - Enum.map(notes, &render(__MODULE__, "show_note.json", &1)) + Enum.map(notes, &render(__MODULE__, "show_note.json", Map.from_struct(&1))) end def render("index_notes.json", _), do: [] -- cgit v1.2.3 From 70880d54f85a96d07b6c72adfbf3f1a7c50f95a2 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 12:55:16 -0500 Subject: @env is not used --- lib/pleroma/application.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index e73d89350..02dd39939 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -99,7 +99,7 @@ defmodule Pleroma.Application do ] ++ task_children(@env) ++ dont_run_in_test(@env) ++ - chat_child(@env, chat_enabled?()) ++ + chat_child(chat_enabled?()) ++ [ Pleroma.Web.Endpoint, Pleroma.Gopher.Server @@ -201,11 +201,11 @@ defmodule Pleroma.Application do ] end - defp chat_child(_env, true) do + defp chat_child(true) do [Pleroma.Web.ChatChannel.ChatChannelState] end - defp chat_child(_, _), do: [] + defp chat_child(_), do: [] defp task_children(:test) do [ -- cgit v1.2.3 From 8156940a49df17c00c05bfe60223b165f9dc034b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 13:28:39 -0500 Subject: Compatibility with phoenix_pubsub 2.0.0 --- lib/pleroma/application.ex | 5 ++++- lib/pleroma/web/web.ex | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 02dd39939..fe94b56f4 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -202,7 +202,10 @@ defmodule Pleroma.Application do end defp chat_child(true) do - [Pleroma.Web.ChatChannel.ChatChannelState] + [ + Pleroma.Web.ChatChannel.ChatChannelState, + {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]} + ] end defp chat_child(_), do: [] diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 4f9281851..c319d223c 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -177,7 +177,7 @@ defmodule Pleroma.Web do def channel do quote do # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - use Phoenix.Channel + import Phoenix.Channel import Pleroma.Web.Gettext end end -- cgit v1.2.3 From d0eca5b12518b0b98ef53003d60b08a78decf35f Mon Sep 17 00:00:00 2001 From: feld Date: Wed, 7 Oct 2020 19:16:53 +0000 Subject: Apply 2 suggestion(s) to 2 file(s) --- lib/pleroma/plugs/remote_ip.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex index 51cc87ad8..987022156 100644 --- a/lib/pleroma/plugs/remote_ip.ex +++ b/lib/pleroma/plugs/remote_ip.ex @@ -43,6 +43,6 @@ defmodule Pleroma.Plugs.RemoteIp do InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128" end - InetCidr.parse(proxy) + InetCidr.parse(proxy, true) end end -- cgit v1.2.3 From 822e4472f3de1492aed6948514bc357c9adb934c Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 15:06:16 -0500 Subject: Fix incorrect use of connect/1 Hint from Phoenix 1.4.17, which has a connect/7 shim: lib/phoenix/socket/transport.ex: def connect(endpoint, handler, _transport_name, transport, serializers, params, _pid \\ self()) do IO.warn "Phoenix.Socket.Transport.connect/7 is deprecated" handler.connect(%{ endpoint: endpoint, transport: transport, options: [serializer: serializers], params: params }) end --- lib/transports.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/transports.ex b/lib/transports.ex index 1ed3a942d..c3665bebe 100644 --- a/lib/transports.ex +++ b/lib/transports.ex @@ -31,7 +31,7 @@ defmodule Phoenix.Transports.WebSocket.Raw do case conn do %{halted: false} = conn -> - case Transport.connect(%{ + case handler.connect(%{ endpoint: endpoint, transport: transport, options: [serializer: nil], -- cgit v1.2.3 From ed6511a086694fc163b488d807f17d246f80ad5b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Wed, 7 Oct 2020 15:28:29 -0500 Subject: Lint --- lib/pleroma/web/mastodon_api/controllers/status_controller.ex | 1 - 1 file changed, 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index c85757f26..a47a7af95 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -155,7 +155,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end end - # Creates a regular status def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) -- cgit v1.2.3 From 9c672ecbb5d4477cd16d2139a2cb66d3923ac5c8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Oct 2020 20:01:48 -0500 Subject: Remote Timeline: add Streaming support --- lib/pleroma/activity/ir/topics.ex | 13 ++++++++++++- lib/pleroma/web/streamer/streamer.ex | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/activity/ir/topics.ex b/lib/pleroma/activity/ir/topics.ex index 9e65bedad..fe2e8cb5c 100644 --- a/lib/pleroma/activity/ir/topics.ex +++ b/lib/pleroma/activity/ir/topics.ex @@ -40,7 +40,8 @@ defmodule Pleroma.Activity.Ir.Topics do end defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do - tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) + tags ++ + remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) end defp item_creation_tags(tags, _, _) do @@ -55,9 +56,19 @@ defmodule Pleroma.Activity.Ir.Topics do defp hashtags_to_topics(_), do: [] + defp remote_topics(%{local: true}), do: [] + + defp remote_topics(%{actor: actor}) when is_binary(actor), + do: ["public:remote:" <> URI.parse(actor).host] + + defp remote_topics(_), do: [] + defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: [] defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"] + defp attachment_topics(_object, %{actor: actor}) when is_binary(actor), + do: ["public:media", "public:remote:media:" <> URI.parse(actor).host] + defp attachment_topics(_object, _act), do: ["public:media"] end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 5475f18a6..d774f0dd9 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -57,6 +57,15 @@ defmodule Pleroma.Web.Streamer do {:ok, "hashtag:" <> tag} end + # Allow remote instance streams. + def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do + {:ok, "public:remote:" <> instance} + end + + def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do + {:ok, "public:remote:media:" <> instance} + end + # Expand user streams. def get_topic( stream, -- cgit v1.2.3 From 3ca98878d27478037233a92f72adb3fbade62035 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 9 Oct 2020 17:08:05 -0500 Subject: Deep link to the user account in AdminFE in account confirmation emails --- lib/pleroma/emails/admin_email.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index c27ad1065..8979db2f8 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -88,7 +88,7 @@ defmodule Pleroma.Emails.AdminEmail do html_body = """

New account for review: @#{account.nickname}

#{HTML.strip_tags(account.registration_reason)}
- Visit AdminFE + Visit AdminFE """ new() -- cgit v1.2.3 From e1eb54d3899883b5af6a43687a2345543d69ad4a Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 11 Oct 2020 13:37:19 +0300 Subject: [#3053] Rollback of access control changes in ActivityPubController (base actions: :user, :object, :activity). --- .../web/activity_pub/activity_pub_controller.ex | 56 ++++++++++------------ 1 file changed, 25 insertions(+), 31 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index c78edfb4c..732c44271 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -32,23 +32,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers] - # Note: :following and :followers must be served even without authentication (as via :api) - @auth_only_actions [:read_inbox, :update_outbox, :whoami, :upload_media] - - # Always accessible actions (must perform entity accessibility checks) - @no_auth_no_federation_actions [:user, :activity, :object] - - @authenticated_or_federating_actions @federating_only_actions ++ - @auth_only_actions ++ @no_auth_no_federation_actions - plug(FederatingPlug when action in @federating_only_actions) - plug(EnsureAuthenticatedPlug when action in @auth_only_actions) - plug( EnsureAuthenticatedPlug, - [unless_func: &FederatingPlug.federating?/1] - when action not in @authenticated_or_federating_actions + [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions + ) + + # Note: :following and :followers must be served even without authentication (as via :api) + plug( + EnsureAuthenticatedPlug + when action in [:read_inbox, :update_outbox, :whoami, :upload_media] ) plug( @@ -72,22 +66,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def user(conn, %{"nickname" => nickname}) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname), - {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)}, {:ok, user} <- User.ensure_keys_present(user) do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("user.json", %{user: user}) else - _ -> {:error, :not_found} + nil -> {:error, :not_found} + %{local: false} -> {:error, :not_found} end end def object(conn, _) do with ap_id <- Endpoint.url() <> conn.request_path, %Object{} = object <- Object.get_cached_by_ap_id(ap_id), - {_, true} <- {:public?, Visibility.is_public?(object)}, - {_, false} <- {:restricted?, Visibility.restrict_unauthenticated_access?(object)} do + {_, true} <- {:public?, Visibility.is_public?(object)} do conn |> assign(:tracking_fun_data, object.id) |> set_cache_ttl_for(object) @@ -95,15 +88,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_view(ObjectView) |> render("object.json", object: object) else - _ -> {:error, :not_found} + {:public?, false} -> + {:error, :not_found} end end + def track_object_fetch(conn, nil), do: conn + + def track_object_fetch(conn, object_id) do + with %{assigns: %{user: %User{id: user_id}}} <- conn do + Delivery.create(object_id, user_id) + end + + conn + end + def activity(conn, _params) do with ap_id <- Endpoint.url() <> conn.request_path, %Activity{} = activity <- Activity.normalize(ap_id), - {_, true} <- {:public?, Visibility.is_public?(activity)}, - {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)} do + {_, true} <- {:public?, Visibility.is_public?(activity)} do conn |> maybe_set_tracking_data(activity) |> set_cache_ttl_for(activity) @@ -111,7 +114,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_view(ObjectView) |> render("object.json", object: activity) else - _ -> {:error, :not_found} + {:public?, false} -> {:error, :not_found} + nil -> {:error, :not_found} end end @@ -546,14 +550,4 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(object.data) end end - - def track_object_fetch(conn, nil), do: conn - - def track_object_fetch(conn, object_id) do - with %{assigns: %{user: %User{id: user_id}}} <- conn do - Delivery.create(object_id, user_id) - end - - conn - end end -- cgit v1.2.3 From 89c595b772eaaa8809f5339d708d7dc22e51b662 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 11 Oct 2020 22:34:28 +0300 Subject: [#3053] Removed target accessibility checks for OStatus endpoints delegating to RedirectController. Added tests. --- lib/pleroma/web/ostatus/ostatus_controller.ex | 13 ++++----- lib/pleroma/web/router.ex | 38 +++++++++++++-------------- 2 files changed, 24 insertions(+), 27 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index b4dc2a87f..e03ca8c0a 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -37,11 +37,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)}, - {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)} do + {_, true} <- {:public?, Visibility.is_public?(activity)} do redirect(conn, to: "/notice/#{activity.id}") else - reason when reason in [{:public?, false}, {:visible?, false}, {:activity, nil}] -> + reason when reason in [{:public?, false}, {:activity, nil}] -> {:error, :not_found} e -> @@ -57,11 +56,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do def activity(conn, _params) do with id <- Endpoint.url() <> conn.request_path, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)}, - {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)} do + {_, true} <- {:public?, Visibility.is_public?(activity)} do redirect(conn, to: "/notice/#{activity.id}") else - reason when reason in [{:public?, false}, {:visible?, false}, {:activity, nil}] -> + reason when reason in [{:public?, false}, {:activity, nil}] -> {:error, :not_found} e -> @@ -72,7 +70,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)}, - {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do cond do format in ["json", "activity+json"] -> @@ -100,7 +97,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do RedirectController.redirector(conn, nil) end else - reason when reason in [{:public?, false}, {:visible?, false}, {:activity, nil}] -> + reason when reason in [{:public?, false}, {:activity, nil}] -> conn |> put_status(404) |> RedirectController.redirector(nil, 404) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 97fcaafd5..ef56360ed 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -9,6 +9,18 @@ defmodule Pleroma.Web.Router 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 @@ -574,24 +586,10 @@ defmodule Pleroma.Web.Router do ) end - pipeline :ostatus_html_json do - plug(:accepts, ["html", "activity+json", "json"]) - plug(Pleroma.Plugs.StaticFEPlug) - end - - pipeline :ostatus_html_xml do - plug(:accepts, ["html", "xml", "rss", "atom"]) - plug(Pleroma.Plugs.StaticFEPlug) - end - - pipeline :ostatus_html_xml_json do - plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"]) - plug(Pleroma.Plugs.StaticFEPlug) - end - scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled - pipe_through(:ostatus_html_json) + # Note: http signature is only considered for json requests (no auth for non-json requests) + pipe_through([:accepts_html_json, :http_signature, Pleroma.Plugs.StaticFEPlug]) get("/objects/:uuid", OStatus.OStatusController, :object) get("/activities/:uuid", OStatus.OStatusController, :activity) @@ -604,15 +602,17 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled - pipe_through(:ostatus_html_xml_json) + # Note: http signature is only considered for json requests (no auth for non-json requests) + pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Plugs.StaticFEPlug]) - # Note: for json format responds with user profile (not user feed) + # 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(:ostatus_html_xml) + pipe_through([:accepts_html_xml, Pleroma.Plugs.StaticFEPlug]) + get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) end -- cgit v1.2.3 From 6c61ef14c3f48910c52e17c68fce175682717962 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 11:18:39 -0500 Subject: Support enabling upload filters during instance gen --- lib/mix/tasks/pleroma/instance.ex | 64 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 91440b453..fc21ae062 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -33,7 +33,10 @@ defmodule Mix.Tasks.Pleroma.Instance do uploads_dir: :string, static_dir: :string, listen_ip: :string, - listen_port: :string + listen_port: :string, + strip_uploads: :string, + anonymize_uploads: :string, + dedupe_uploads: :string ], aliases: [ o: :output, @@ -158,6 +161,30 @@ defmodule Mix.Tasks.Pleroma.Instance do ) |> Path.expand() + strip_uploads = + get_option( + options, + :strip_uploads, + "Do you want to strip location (GPS) data from uploaded images? (y/n)", + "y" + ) === "y" + + anonymize_uploads = + get_option( + options, + :anonymize_uploads, + "Do you want to anonymize the filenames of uploads? (y/n)", + "n" + ) === "y" + + dedupe_uploads = + get_option( + options, + :dedupe_uploads, + "Do you want to deduplicate uploaded files? (y/n)", + "n" + ) === "y" + Config.put([:instance, :static_dir], static_dir) secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) @@ -188,7 +215,13 @@ defmodule Mix.Tasks.Pleroma.Instance do uploads_dir: uploads_dir, rum_enabled: rum_enabled, listen_ip: listen_ip, - listen_port: listen_port + listen_port: listen_port, + upload_filters: + upload_filters(%{ + strip: strip_uploads, + anonymize: anonymize_uploads, + dedupe: dedupe_uploads + }) ) result_psql = @@ -247,4 +280,31 @@ defmodule Mix.Tasks.Pleroma.Instance do File.write(robots_txt_path, robots_txt) shell_info("Writing #{robots_txt_path}.") end + + defp upload_filters(filters) when is_map(filters) do + enabled_filters = + if filters.strip do + [Pleroma.Upload.Filter.ExifTool] + else + [] + end + + enabled_filters = + if filters.anonymize do + enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename] + else + enabled_filters + end + + enabled_filters = + if filters.dedupe do + enabled_filters ++ [Pleroma.Upload.Filter.Dedupe] + else + enabled_filters + end + + enabled_filters + end + + defp upload_filters(_), do: [] end -- cgit v1.2.3 From 8539e386c3f00537f120487e717ec7b25fe6c572 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Oct 2020 12:00:50 -0500 Subject: Add missing Copyright headers --- lib/mix/tasks/pleroma/count_statuses.ex | 4 ++++ lib/mix/tasks/pleroma/digest.ex | 4 ++++ lib/mix/tasks/pleroma/docs.ex | 4 ++++ lib/mix/tasks/pleroma/email.ex | 4 ++++ lib/mix/tasks/pleroma/notification_settings.ex | 4 ++++ lib/pleroma/config/oban.ex | 4 ++++ lib/pleroma/docs/generator.ex | 4 ++++ lib/pleroma/docs/json.ex | 4 ++++ lib/pleroma/docs/markdown.ex | 4 ++++ lib/pleroma/emoji/pack.ex | 4 ++++ lib/pleroma/gun/connection_pool.ex | 4 ++++ lib/pleroma/gun/connection_pool/reclaimer.ex | 4 ++++ lib/pleroma/gun/connection_pool/worker.ex | 4 ++++ lib/pleroma/gun/connection_pool/worker_supervisor.ex | 4 ++++ lib/pleroma/http/adapter_helper/default.ex | 4 ++++ lib/pleroma/http/adapter_helper/hackney.ex | 4 ++++ lib/pleroma/jwt.ex | 4 ++++ lib/pleroma/moderation_log.ex | 4 ++++ lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex | 4 ++++ lib/pleroma/plugs/rate_limiter/supervisor.ex | 4 ++++ lib/pleroma/telemetry/logger.ex | 4 ++++ lib/pleroma/web/activity_pub/builder.ex | 4 ++++ lib/pleroma/web/activity_pub/side_effects.ex | 4 ++++ lib/pleroma/web/mailer/subscription_controller.ex | 4 ++++ lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex | 4 ++++ lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex | 4 ++++ lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex | 4 ++++ lib/pleroma/web/rich_media/parsers/ttl/ttl.ex | 4 ++++ lib/pleroma/web/views/email_view.ex | 4 ++++ lib/pleroma/web/views/mailer/subscription_view.ex | 4 ++++ 30 files changed, 120 insertions(+) (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/count_statuses.ex b/lib/mix/tasks/pleroma/count_statuses.ex index e1e8195dd..8761d8f17 100644 --- a/lib/mix/tasks/pleroma/count_statuses.ex +++ b/lib/mix/tasks/pleroma/count_statuses.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.CountStatuses do @shortdoc "Re-counts statuses for all users" diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 3595f912d..cac148b88 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Digest do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex index 6088fc71d..ad5c37fc9 100644 --- a/lib/mix/tasks/pleroma/docs.ex +++ b/lib/mix/tasks/pleroma/docs.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Docs do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/email.ex b/lib/mix/tasks/pleroma/email.ex index 9972cb988..bc5facc09 100644 --- a/lib/mix/tasks/pleroma/email.ex +++ b/lib/mix/tasks/pleroma/email.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.Email do use Mix.Task import Mix.Pleroma diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex index 00f5ba7bf..f99275de1 100644 --- a/lib/mix/tasks/pleroma/notification_settings.ex +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Mix.Tasks.Pleroma.NotificationSettings do @shortdoc "Enable&Disable privacy option for push notifications" @moduledoc """ diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex index 9f601b1a3..8e0351d52 100644 --- a/lib/pleroma/config/oban.ex +++ b/lib/pleroma/config/oban.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Config.Oban do require Logger diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index a671a6278..a70f83b73 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.Generator do @callback process(keyword()) :: {:ok, String.t()} diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index feeb4320e..13618b509 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.JSON do @behaviour Pleroma.Docs.Generator @external_resource "config/description.exs" diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex index da3f20f43..eac0789a6 100644 --- a/lib/pleroma/docs/markdown.ex +++ b/lib/pleroma/docs/markdown.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Docs.Markdown do @behaviour Pleroma.Docs.Generator diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 8f1989ada..0670f29f1 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Emoji.Pack do @derive {Jason.Encoder, only: [:files, :pack, :files_count]} defstruct files: %{}, diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex index f34602b73..e322f192a 100644 --- a/lib/pleroma/gun/connection_pool.ex +++ b/lib/pleroma/gun/connection_pool.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool do @registry __MODULE__ diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex index cea800882..241e8b04f 100644 --- a/lib/pleroma/gun/connection_pool/reclaimer.ex +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.Reclaimer do use GenServer, restart: :temporary diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index bf57e9e5f..b71816bed 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.Worker do alias Pleroma.Gun use GenServer, restart: :temporary diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex index 39615c956..4c23bcbd9 100644 --- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex +++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit" diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex index e13441316..8567a616b 100644 --- a/lib/pleroma/http/adapter_helper/default.ex +++ b/lib/pleroma/http/adapter_helper/default.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTTP.AdapterHelper.Default do alias Pleroma.HTTP.AdapterHelper diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index ef84553c1..ff60513fd 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTTP.AdapterHelper.Hackney do @behaviour Pleroma.HTTP.AdapterHelper diff --git a/lib/pleroma/jwt.ex b/lib/pleroma/jwt.ex index 10102ff5d..faeb77781 100644 --- a/lib/pleroma/jwt.ex +++ b/lib/pleroma/jwt.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.JWT do use Joken.Config diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 47036a6f6..38a863443 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ModerationLog do use Ecto.Schema diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex index 884268d96..0bf5aadfb 100644 --- a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex +++ b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do use DynamicSupervisor diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex index 9672f7876..ce196df52 100644 --- a/lib/pleroma/plugs/rate_limiter/supervisor.ex +++ b/lib/pleroma/plugs/rate_limiter/supervisor.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.RateLimiter.Supervisor do use Supervisor diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 197b1d091..003079cf3 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Telemetry.Logger do @moduledoc "Transforms Pleroma telemetry events to logs" diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 9a7b7d9de..298aff6b7 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.Builder do @moduledoc """ This module builds the objects. Meant to be used for creating local objects. diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b9a83a544..2eec0ce86 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.SideEffects do @moduledoc """ This module looks at an inserted object and executes the side effects that it diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex index 478a83518..ace44afd1 100644 --- a/lib/pleroma/web/mailer/subscription_controller.ex +++ b/lib/pleroma/web/mailer/subscription_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Mailer.SubscriptionController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex index 71c53df1d..7c0345094 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.EmojiFileController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index 6696f8b92..a0e5c739a 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.PleromaAPI.EmojiPackController do use Pleroma.Web, :controller diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex index c5aaea2d4..15109d28d 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do @behaviour Pleroma.Web.RichMedia.Parser.TTL diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex index 6b3ec6d30..13511888c 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parser.TTL do @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} end diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex index 6b0fbe61e..bcdee6571 100644 --- a/lib/pleroma/web/views/email_view.ex +++ b/lib/pleroma/web/views/email_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.EmailView do use Pleroma.Web, :view import Phoenix.HTML diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex index fc3d20816..4562a9d6c 100644 --- a/lib/pleroma/web/views/mailer/subscription_view.ex +++ b/lib/pleroma/web/views/mailer/subscription_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Mailer.SubscriptionView do use Pleroma.Web, :view end -- cgit v1.2.3 From 83ae45b000261d3e03a4b554064350a5ead172c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 12 Oct 2020 18:49:37 -0500 Subject: Preload `/api/pleroma/frontend_configurations`, fixes #1932 --- lib/pleroma/web/preload/instance.ex | 9 +++++++++ lib/pleroma/web/twitter_api/controllers/util_controller.ex | 6 +----- lib/pleroma/web/twitter_api/views/util_view.ex | 6 ++++++ 3 files changed, 16 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex index 50d1f3382..cc6f8cf99 100644 --- a/lib/pleroma/web/preload/instance.ex +++ b/lib/pleroma/web/preload/instance.ex @@ -7,11 +7,13 @@ defmodule Pleroma.Web.Preload.Providers.Instance do alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Preload.Providers.Provider + alias Pleroma.Web.TwitterAPI.UtilView @behaviour Provider @instance_url "/api/v1/instance" @panel_url "/instance/panel.html" @nodeinfo_url "/nodeinfo/2.0.json" + @fe_config_url "/api/pleroma/frontend_configurations" @impl Provider def generate_terms(_params) do @@ -19,6 +21,7 @@ defmodule Pleroma.Web.Preload.Providers.Instance do |> build_info_tag() |> build_panel_tag() |> build_nodeinfo_tag() + |> build_fe_config_tag() end defp build_info_tag(acc) do @@ -47,4 +50,10 @@ defmodule Pleroma.Web.Preload.Providers.Instance do Map.put(acc, @nodeinfo_url, nodeinfo_data) end end + + defp build_fe_config_tag(acc) do + fe_data = UtilView.render("frontend_configurations.json", %{}) + + Map.put(acc, @fe_config_url, fe_data) + 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 70b0fbd54..6d827846d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -74,11 +74,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end def frontend_configurations(conn, _params) do - config = - Config.get(:frontend_configurations, %{}) - |> Enum.into(%{}) - - json(conn, config) + render(conn, "frontend_configurations.json") end def emoji(conn, _params) do diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index d3bdb4f62..98eea1d18 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.Config alias Pleroma.Web def status_net_config(instance) do @@ -19,4 +20,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do """ end + + def render("frontend_configurations.json", _) do + Config.get(:frontend_configurations, %{}) + |> Enum.into(%{}) + end end -- cgit v1.2.3 From b573711e9c8200ecdd4a722ce1e02b48d3f74cce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 20 Jun 2020 18:37:44 +0300 Subject: file locations consistency --- lib/credo/check/consistency/file_location.ex | 132 +++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 lib/credo/check/consistency/file_location.ex (limited to 'lib') diff --git a/lib/credo/check/consistency/file_location.ex b/lib/credo/check/consistency/file_location.ex new file mode 100644 index 000000000..5ef17b894 --- /dev/null +++ b/lib/credo/check/consistency/file_location.ex @@ -0,0 +1,132 @@ +defmodule Credo.Check.Consistency.FileLocation do + @moduledoc false + + # credo:disable-for-this-file Credo.Check.Readability.Specs + + @checkdoc """ + File location should follow the namespace hierarchy of the module it defines. + + Examples: + + - `lib/my_system.ex` should define the `MySystem` module + - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module + """ + @explanation [warning: @checkdoc] + + # `use Credo.Check` required that module attributes are already defined, so we need to place these attributes + # before use/alias expressions. + # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout + use Credo.Check, category: :warning, base_priority: :high + + alias Credo.Code + + def run(source_file, params \\ []) do + case verify(source_file, params) do + :ok -> + [] + + {:error, module, expected_file} -> + error(IssueMeta.for(source_file, params), module, expected_file) + end + end + + defp verify(source_file, params) do + source_file.filename + |> Path.relative_to_cwd() + |> verify(Code.ast(source_file), params) + end + + @doc false + def verify(relative_path, ast, params) do + if verify_path?(relative_path, params), + do: ast |> main_module() |> verify_module(relative_path, params), + else: :ok + end + + defp verify_path?(relative_path, params) do + case Path.split(relative_path) do + ["lib" | _] -> not exclude?(relative_path, params) + ["test", "support" | _] -> false + ["test", "test_helper.exs"] -> false + ["test" | _] -> not exclude?(relative_path, params) + _ -> false + end + end + + defp exclude?(relative_path, params) do + params + |> Keyword.get(:exclude, []) + |> Enum.any?(&String.starts_with?(relative_path, &1)) + end + + defp main_module(ast) do + {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) + Enum.at(modules, -1) + end + + defp traverse({:defmodule, _meta, args}, modules) do + [{:__aliases__, _, name_parts}, _module_body] = args + {args, [Module.concat(name_parts) | modules]} + end + + defp traverse(ast, state), do: {ast, state} + + # empty file - shouldn't really happen, but we'll let it through + defp verify_module(nil, _relative_path, _params), do: :ok + + defp verify_module(main_module, relative_path, params) do + parsed_path = parsed_path(relative_path, params) + + expected_file = + expected_file_base(parsed_path.root, main_module) <> + Path.extname(parsed_path.allowed) + + if expected_file == parsed_path.allowed, + do: :ok, + else: {:error, main_module, expected_file} + end + + defp parsed_path(relative_path, params) do + parts = Path.split(relative_path) + + allowed = + Keyword.get(params, :ignore_folder_namespace, %{}) + |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) + |> Stream.map(&Path.split/1) + |> Enum.find(&List.starts_with?(parts, &1)) + |> case do + nil -> + relative_path + + ignore_parts -> + Stream.drop(ignore_parts, -1) + |> Enum.concat(Stream.drop(parts, length(ignore_parts))) + |> Path.join() + end + + %{root: hd(parts), allowed: allowed} + end + + defp expected_file_base(root_folder, module) do + {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) + + relative_path = + if parent_namespace == [], + do: "", + else: parent_namespace |> Module.concat() |> Macro.underscore() + + file_name = module_name |> Module.concat() |> Macro.underscore() + + Path.join([root_folder, relative_path, file_name]) + end + + defp error(issue_meta, module, expected_file) do + format_issue(issue_meta, + message: + "Mismatch between file name and main module #{inspect(module)}. " <> + "Expected file path to be #{expected_file}. " <> + "Either move the file or rename the module.", + line_no: 1 + ) + end +end -- cgit v1.2.3 From 6bf85440b373c9b2fa1e8e7184dcf87518600306 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 20 Jun 2020 19:09:04 +0300 Subject: mix tasks consistency --- lib/mix/tasks/pleroma/ecto.ex | 50 +++++++++++++++++++++++++++++++++++++ lib/mix/tasks/pleroma/ecto/ecto.ex | 50 ------------------------------------- lib/mix/tasks/pleroma/robots_txt.ex | 33 ++++++++++++++++++++++++ lib/mix/tasks/pleroma/robotstxt.ex | 33 ------------------------ 4 files changed, 83 insertions(+), 83 deletions(-) create mode 100644 lib/mix/tasks/pleroma/ecto.ex delete mode 100644 lib/mix/tasks/pleroma/ecto/ecto.ex create mode 100644 lib/mix/tasks/pleroma/robots_txt.ex delete mode 100644 lib/mix/tasks/pleroma/robotstxt.ex (limited to 'lib') diff --git a/lib/mix/tasks/pleroma/ecto.ex b/lib/mix/tasks/pleroma/ecto.ex new file mode 100644 index 000000000..3363cd45f --- /dev/null +++ b/lib/mix/tasks/pleroma/ecto.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-onl + +defmodule Mix.Tasks.Pleroma.Ecto do + @doc """ + Ensures the given repository's migrations path exists on the file system. + """ + @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() + def ensure_migrations_path(repo, opts) do + path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") + + path = + case Path.type(path) do + :relative -> + Path.join(Application.app_dir(:pleroma), path) + + :absolute -> + path + end + + if not File.dir?(path) do + raise_missing_migrations(Path.relative_to_cwd(path), repo) + end + + path + end + + @doc """ + Returns the private repository path relative to the source. + """ + def source_repo_priv(repo) do + config = repo.config() + priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" + Path.join(Application.app_dir(:pleroma), priv) + end + + defp raise_missing_migrations(path, repo) do + raise(""" + Could not find migrations directory #{inspect(path)} + for repo #{inspect(repo)}. + This may be because you are in a new project and the + migration directory has not been created yet. Creating an + empty directory at the path above will fix this error. + If you expected existing migrations to be found, please + make sure your repository has been properly configured + and the configured path exists. + """) + end +end diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto/ecto.ex deleted file mode 100644 index 3363cd45f..000000000 --- a/lib/mix/tasks/pleroma/ecto/ecto.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-onl - -defmodule Mix.Tasks.Pleroma.Ecto do - @doc """ - Ensures the given repository's migrations path exists on the file system. - """ - @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t() - def ensure_migrations_path(repo, opts) do - path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations") - - path = - case Path.type(path) do - :relative -> - Path.join(Application.app_dir(:pleroma), path) - - :absolute -> - path - end - - if not File.dir?(path) do - raise_missing_migrations(Path.relative_to_cwd(path), repo) - end - - path - end - - @doc """ - Returns the private repository path relative to the source. - """ - def source_repo_priv(repo) do - config = repo.config() - priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}" - Path.join(Application.app_dir(:pleroma), priv) - end - - defp raise_missing_migrations(path, repo) do - raise(""" - Could not find migrations directory #{inspect(path)} - for repo #{inspect(repo)}. - This may be because you are in a new project and the - migration directory has not been created yet. Creating an - empty directory at the path above will fix this error. - If you expected existing migrations to be found, please - make sure your repository has been properly configured - and the configured path exists. - """) - end -end diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex new file mode 100644 index 000000000..24f08180e --- /dev/null +++ b/lib/mix/tasks/pleroma/robots_txt.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RobotsTxt do + use Mix.Task + + @shortdoc "Generate robots.txt" + @moduledoc """ + Generates robots.txt + + ## Overwrite robots.txt to disallow all + + mix pleroma.robots_txt disallow_all + + This will write a robots.txt that will hide all paths on your instance + from search engines and other robots that obey robots.txt + + """ + def run(["disallow_all"]) do + Mix.Pleroma.start_pleroma() + static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") + + if !File.exists?(static_dir) do + File.mkdir_p!(static_dir) + end + + robots_txt_path = Path.join(static_dir, "robots.txt") + robots_txt_content = "User-Agent: *\nDisallow: /\n" + + File.write!(robots_txt_path, robots_txt_content, [:write]) + end +end diff --git a/lib/mix/tasks/pleroma/robotstxt.ex b/lib/mix/tasks/pleroma/robotstxt.ex deleted file mode 100644 index 24f08180e..000000000 --- a/lib/mix/tasks/pleroma/robotstxt.ex +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Mix.Tasks.Pleroma.RobotsTxt do - use Mix.Task - - @shortdoc "Generate robots.txt" - @moduledoc """ - Generates robots.txt - - ## Overwrite robots.txt to disallow all - - mix pleroma.robots_txt disallow_all - - This will write a robots.txt that will hide all paths on your instance - from search engines and other robots that obey robots.txt - - """ - def run(["disallow_all"]) do - Mix.Pleroma.start_pleroma() - static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") - - if !File.exists?(static_dir) do - File.mkdir_p!(static_dir) - end - - robots_txt_path = Path.join(static_dir, "robots.txt") - robots_txt_content = "User-Agent: *\nDisallow: /\n" - - File.write!(robots_txt_path, robots_txt_content, [:write]) - end -end -- cgit v1.2.3 From 103f3dcb9ed0a12a11e9cc5c574449439fc2cb0e Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 18:33:03 +0300 Subject: rich media parser ttl files consistency --- lib/pleroma/web/rich_media/parser/ttl.ex | 7 +++ .../web/rich_media/parser/ttl/aws_signed_url.ex | 50 ++++++++++++++++++++++ .../web/rich_media/parsers/ttl/aws_signed_url.ex | 50 ---------------------- lib/pleroma/web/rich_media/parsers/ttl/ttl.ex | 7 --- 4 files changed, 57 insertions(+), 57 deletions(-) create mode 100644 lib/pleroma/web/rich_media/parser/ttl.ex create mode 100644 lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/ttl/ttl.ex (limited to 'lib') diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex new file mode 100644 index 000000000..8353f0fff --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/ttl.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL do + @callback ttl(Map.t(), String.t()) :: Integer.t() | nil +end diff --git a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex new file mode 100644 index 000000000..fc4ef79c0 --- /dev/null +++ b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do + @behaviour Pleroma.Web.RichMedia.Parser.TTL + + @impl true + def ttl(data, _url) do + image = Map.get(data, :image) + + if is_aws_signed_url(image) do + image + |> parse_query_params() + |> format_query_params() + |> get_expiration_timestamp() + else + {:error, "Not aws signed url #{inspect(image)}"} + end + end + + defp is_aws_signed_url(image) when is_binary(image) and image != "" do + %URI{host: host, query: query} = URI.parse(image) + + String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") + end + + defp is_aws_signed_url(_), do: nil + + defp parse_query_params(image) do + %URI{query: query} = URI.parse(image) + query + end + + defp format_query_params(query) do + query + |> String.split(~r/&|=/) + |> Enum.chunk_every(2) + |> Map.new(fn [k, v] -> {k, v} end) + end + + defp get_expiration_timestamp(params) when is_map(params) do + {:ok, date} = + params + |> Map.get("X-Amz-Date") + |> Timex.parse("{ISO:Basic:Z}") + + {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} + end +end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex deleted file mode 100644 index 15109d28d..000000000 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ /dev/null @@ -1,50 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do - @behaviour Pleroma.Web.RichMedia.Parser.TTL - - @impl Pleroma.Web.RichMedia.Parser.TTL - def ttl(data, _url) do - image = Map.get(data, :image) - - if is_aws_signed_url(image) do - image - |> parse_query_params() - |> format_query_params() - |> get_expiration_timestamp() - else - {:error, "Not aws signed url #{inspect(image)}"} - end - end - - defp is_aws_signed_url(image) when is_binary(image) and image != "" do - %URI{host: host, query: query} = URI.parse(image) - - String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") - end - - defp is_aws_signed_url(_), do: nil - - defp parse_query_params(image) do - %URI{query: query} = URI.parse(image) - query - end - - defp format_query_params(query) do - query - |> String.split(~r/&|=/) - |> Enum.chunk_every(2) - |> Map.new(fn [k, v] -> {k, v} end) - end - - defp get_expiration_timestamp(params) when is_map(params) do - {:ok, date} = - params - |> Map.get("X-Amz-Date") - |> Timex.parse("{ISO:Basic:Z}") - - {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))} - end -end diff --git a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex b/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex deleted file mode 100644 index 13511888c..000000000 --- a/lib/pleroma/web/rich_media/parsers/ttl/ttl.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.RichMedia.Parser.TTL do - @callback ttl(Map.t(), String.t()) :: {:ok, Integer.t()} | {:error, String.t()} -end -- cgit v1.2.3 From b5b4395e4a7c63e31579475888fa892dcdaeecff Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 19:08:19 +0300 Subject: oauth consistency --- lib/pleroma/plugs/o_auth_plug.ex | 120 ++++ lib/pleroma/plugs/o_auth_scopes_plug.ex | 77 +++ lib/pleroma/plugs/oauth_plug.ex | 120 ---- lib/pleroma/plugs/oauth_scopes_plug.ex | 77 --- .../admin_api/controllers/o_auth_app_controller.ex | 77 +++ .../admin_api/controllers/oauth_app_controller.ex | 77 --- .../operations/admin/o_auth_app_operation.ex | 217 ++++++++ .../operations/admin/oauth_app_operation.ex | 217 -------- lib/pleroma/web/o_auth.ex | 6 + lib/pleroma/web/o_auth/app.ex | 149 +++++ lib/pleroma/web/o_auth/authorization.ex | 95 ++++ lib/pleroma/web/o_auth/fallback_controller.ex | 32 ++ lib/pleroma/web/o_auth/mfa_controller.ex | 98 ++++ lib/pleroma/web/o_auth/mfa_view.ex | 17 + lib/pleroma/web/o_auth/o_auth_controller.ex | 610 +++++++++++++++++++++ lib/pleroma/web/o_auth/o_auth_view.ex | 30 + lib/pleroma/web/o_auth/scopes.ex | 76 +++ lib/pleroma/web/o_auth/token.ex | 135 +++++ lib/pleroma/web/o_auth/token/query.ex | 49 ++ .../web/o_auth/token/strategy/refresh_token.ex | 58 ++ lib/pleroma/web/o_auth/token/strategy/revoke.ex | 26 + lib/pleroma/web/o_auth/token/utils.ex | 72 +++ lib/pleroma/web/oauth.ex | 6 - lib/pleroma/web/oauth/app.ex | 149 ----- lib/pleroma/web/oauth/authorization.ex | 95 ---- lib/pleroma/web/oauth/fallback_controller.ex | 32 -- lib/pleroma/web/oauth/mfa_controller.ex | 98 ---- lib/pleroma/web/oauth/mfa_view.ex | 17 - lib/pleroma/web/oauth/oauth_controller.ex | 610 --------------------- lib/pleroma/web/oauth/oauth_view.ex | 30 - lib/pleroma/web/oauth/scopes.ex | 76 --- lib/pleroma/web/oauth/token.ex | 135 ----- lib/pleroma/web/oauth/token/query.ex | 49 -- .../web/oauth/token/strategy/refresh_token.ex | 58 -- lib/pleroma/web/oauth/token/strategy/revoke.ex | 26 - lib/pleroma/web/oauth/token/utils.ex | 72 --- 36 files changed, 1944 insertions(+), 1944 deletions(-) create mode 100644 lib/pleroma/plugs/o_auth_plug.ex create mode 100644 lib/pleroma/plugs/o_auth_scopes_plug.ex delete mode 100644 lib/pleroma/plugs/oauth_plug.ex delete mode 100644 lib/pleroma/plugs/oauth_scopes_plug.ex create mode 100644 lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex delete mode 100644 lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex delete mode 100644 lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex create mode 100644 lib/pleroma/web/o_auth.ex create mode 100644 lib/pleroma/web/o_auth/app.ex create mode 100644 lib/pleroma/web/o_auth/authorization.ex create mode 100644 lib/pleroma/web/o_auth/fallback_controller.ex create mode 100644 lib/pleroma/web/o_auth/mfa_controller.ex create mode 100644 lib/pleroma/web/o_auth/mfa_view.ex create mode 100644 lib/pleroma/web/o_auth/o_auth_controller.ex create mode 100644 lib/pleroma/web/o_auth/o_auth_view.ex create mode 100644 lib/pleroma/web/o_auth/scopes.ex create mode 100644 lib/pleroma/web/o_auth/token.ex create mode 100644 lib/pleroma/web/o_auth/token/query.ex create mode 100644 lib/pleroma/web/o_auth/token/strategy/refresh_token.ex create mode 100644 lib/pleroma/web/o_auth/token/strategy/revoke.ex create mode 100644 lib/pleroma/web/o_auth/token/utils.ex delete mode 100644 lib/pleroma/web/oauth.ex delete mode 100644 lib/pleroma/web/oauth/app.ex delete mode 100644 lib/pleroma/web/oauth/authorization.ex delete mode 100644 lib/pleroma/web/oauth/fallback_controller.ex delete mode 100644 lib/pleroma/web/oauth/mfa_controller.ex delete mode 100644 lib/pleroma/web/oauth/mfa_view.ex delete mode 100644 lib/pleroma/web/oauth/oauth_controller.ex delete mode 100644 lib/pleroma/web/oauth/oauth_view.ex delete mode 100644 lib/pleroma/web/oauth/scopes.ex delete mode 100644 lib/pleroma/web/oauth/token.ex delete mode 100644 lib/pleroma/web/oauth/token/query.ex delete mode 100644 lib/pleroma/web/oauth/token/strategy/refresh_token.ex delete mode 100644 lib/pleroma/web/oauth/token/strategy/revoke.ex delete mode 100644 lib/pleroma/web/oauth/token/utils.ex (limited to 'lib') diff --git a/lib/pleroma/plugs/o_auth_plug.ex b/lib/pleroma/plugs/o_auth_plug.ex new file mode 100644 index 000000000..6fa71ef47 --- /dev/null +++ b/lib/pleroma/plugs/o_auth_plug.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthPlug do + import Plug.Conn + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") + + def init(options), do: options + + def call(%{assigns: %{user: %User{}}} = conn, _), do: conn + + def call(%{params: %{"access_token" => access_token}} = conn, _) do + with {:ok, user, token_record} <- fetch_user_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + end + + def call(conn, _) do + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn + end + end + + # Gets user by token + # + @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil + defp fetch_user_and_token(token) do + query = + from(t in Token, + where: t.token == ^token, + join: user in assoc(t, :user), + preload: [user: user] + ) + + # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength + with %Token{user: user} = token_record <- Repo.one(query) do + {:ok, user, token_record} + end + end + + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + + # Gets token from session by :oauth_token key + # + @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_from_session(conn) do + case get_session(conn, :oauth_token) do + nil -> :no_token_found + token -> {:ok, token} + end + end + + # Gets token from headers + # + @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str(%Plug.Conn{} = conn) do + headers = get_req_header(conn, "authorization") + + with :no_token_found <- fetch_token_str(headers), + do: fetch_token_from_session(conn) + end + + @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} + defp fetch_token_str([]), do: :no_token_found + + defp fetch_token_str([token | tail]) do + trimmed_token = String.trim(token) + + case Regex.run(@realm_reg, trimmed_token) do + [_, match] -> {:ok, String.trim(match)} + _ -> fetch_token_str(tail) + end + end +end diff --git a/lib/pleroma/plugs/o_auth_scopes_plug.ex b/lib/pleroma/plugs/o_auth_scopes_plug.ex new file mode 100644 index 000000000..b1a736d78 --- /dev/null +++ b/lib/pleroma/plugs/o_auth_scopes_plug.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthScopesPlug do + import Plug.Conn + import Pleroma.Web.Gettext + + alias Pleroma.Config + + use Pleroma.Web, :plug + + def init(%{scopes: _} = options), do: options + + @impl true + def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do + op = options[:op] || :| + token = assigns[:token] + + scopes = transform_scopes(scopes, options) + matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] + + cond do + token && op == :| && Enum.any?(matched_scopes) -> + conn + + token && op == :& && matched_scopes == scopes -> + conn + + options[:fallback] == :proceed_unauthenticated -> + drop_auth_info(conn) + + true -> + missing_scopes = scopes -- matched_scopes + permissions = Enum.join(missing_scopes, " #{op} ") + + error_message = + dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) + + conn + |> put_resp_content_type("application/json") + |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) + |> halt() + end + end + + @doc "Drops authentication info from connection" + def drop_auth_info(conn) do + # To simplify debugging, setting a private variable on `conn` if auth info is dropped + conn + |> put_private(:authentication_ignored, true) + |> assign(:user, nil) + |> assign(:token, nil) + end + + @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" + def filter_descendants(scopes, supported_scopes) do + Enum.filter( + scopes, + fn scope -> + Enum.find( + supported_scopes, + &(scope == &1 || String.starts_with?(scope, &1 <> ":")) + ) + end + ) + end + + @doc "Transforms scopes by applying supported options (e.g. :admin)" + def transform_scopes(scopes, options) do + if options[:admin] do + Config.oauth_admin_scopes(scopes) + else + scopes + end + end +end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex deleted file mode 100644 index 6fa71ef47..000000000 --- a/lib/pleroma/plugs/oauth_plug.ex +++ /dev/null @@ -1,120 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthPlug do - import Plug.Conn - import Ecto.Query - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") - - def init(options), do: options - - def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - - def call(%{params: %{"access_token" => access_token}} = conn, _) do - with {:ok, user, token_record} <- fetch_user_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(access_token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - end - - def call(conn, _) do - case fetch_token_str(conn) do - {:ok, token} -> - with {:ok, user, token_record} <- fetch_user_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> - # token found, but maybe only with app - with {:ok, app, token_record} <- fetch_app_and_token(token) do - conn - |> assign(:token, token_record) - |> assign(:app, app) - else - _ -> conn - end - end - - _ -> - conn - end - end - - # Gets user by token - # - @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil - defp fetch_user_and_token(token) do - query = - from(t in Token, - where: t.token == ^token, - join: user in assoc(t, :user), - preload: [user: user] - ) - - # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: user} = token_record <- Repo.one(query) do - {:ok, user, token_record} - end - end - - @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil - defp fetch_app_and_token(token) do - query = - from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) - - with %Token{app: app} = token_record <- Repo.one(query) do - {:ok, app, token_record} - end - end - - # Gets token from session by :oauth_token key - # - @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_from_session(conn) do - case get_session(conn, :oauth_token) do - nil -> :no_token_found - token -> {:ok, token} - end - end - - # Gets token from headers - # - @spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str(%Plug.Conn{} = conn) do - headers = get_req_header(conn, "authorization") - - with :no_token_found <- fetch_token_str(headers), - do: fetch_token_from_session(conn) - end - - @spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()} - defp fetch_token_str([]), do: :no_token_found - - defp fetch_token_str([token | tail]) do - trimmed_token = String.trim(token) - - case Regex.run(@realm_reg, trimmed_token) do - [_, match] -> {:ok, String.trim(match)} - _ -> fetch_token_str(tail) - end - end -end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex deleted file mode 100644 index b1a736d78..000000000 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Plugs.OAuthScopesPlug do - import Plug.Conn - import Pleroma.Web.Gettext - - alias Pleroma.Config - - use Pleroma.Web, :plug - - def init(%{scopes: _} = options), do: options - - @impl true - def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do - op = options[:op] || :| - token = assigns[:token] - - scopes = transform_scopes(scopes, options) - matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] - - cond do - token && op == :| && Enum.any?(matched_scopes) -> - conn - - token && op == :& && matched_scopes == scopes -> - conn - - options[:fallback] == :proceed_unauthenticated -> - drop_auth_info(conn) - - true -> - missing_scopes = scopes -- matched_scopes - permissions = Enum.join(missing_scopes, " #{op} ") - - error_message = - dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) - - conn - |> put_resp_content_type("application/json") - |> send_resp(:forbidden, Jason.encode!(%{error: error_message})) - |> halt() - end - end - - @doc "Drops authentication info from connection" - def drop_auth_info(conn) do - # To simplify debugging, setting a private variable on `conn` if auth info is dropped - conn - |> put_private(:authentication_ignored, true) - |> assign(:user, nil) - |> assign(:token, nil) - end - - @doc "Keeps those of `scopes` which are descendants of `supported_scopes`" - def filter_descendants(scopes, supported_scopes) do - Enum.filter( - scopes, - fn scope -> - Enum.find( - supported_scopes, - &(scope == &1 || String.starts_with?(scope, &1 <> ":")) - ) - end - ) - end - - @doc "Transforms scopes by applying supported options (e.g. :admin)" - def transform_scopes(scopes, options) do - if options[:admin] do - Config.oauth_admin_scopes(scopes) - else - scopes - end - end -end diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex new file mode 100644 index 000000000..dca23ea73 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.OAuth.App + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:put_view, Pleroma.Web.MastodonAPI.AppView) + + plug( + OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [:create, :index, :update, :delete] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation + + def index(conn, params) do + search_params = + params + |> Map.take([:client_id, :page, :page_size, :trusted]) + |> Map.put(:client_name, params[:name]) + + with {:ok, apps, count} <- App.search(search_params) do + render(conn, "index.json", + apps: apps, + count: count, + page_size: params.page_size, + admin: true + ) + end + end + + def create(%{body_params: params} = conn, _) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + case App.create(params) do + {:ok, app} -> + render(conn, "show.json", app: app, admin: true) + + {:error, changeset} -> + json(conn, App.errors(changeset)) + end + end + + def update(%{body_params: params} = conn, %{id: id}) do + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) + + with {:ok, app} <- App.update(id, params) do + render(conn, "show.json", app: app, admin: true) + else + {:error, changeset} -> + json(conn, App.errors(changeset)) + + nil -> + json_response(conn, :bad_request, "") + end + end + + def delete(conn, params) do + with {:ok, _app} <- App.destroy(params.id) do + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex deleted file mode 100644 index dca23ea73..000000000 --- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex +++ /dev/null @@ -1,77 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.AdminAPI.OAuthAppController do - use Pleroma.Web, :controller - - import Pleroma.Web.ControllerHelper, only: [json_response: 3] - - alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.Web.OAuth.App - - require Logger - - plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.AppView) - - plug( - OAuthScopesPlug, - %{scopes: ["write"], admin: true} - when action in [:create, :index, :update, :delete] - ) - - action_fallback(Pleroma.Web.AdminAPI.FallbackController) - - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.OAuthAppOperation - - def index(conn, params) do - search_params = - params - |> Map.take([:client_id, :page, :page_size, :trusted]) - |> Map.put(:client_name, params[:name]) - - with {:ok, apps, count} <- App.search(search_params) do - render(conn, "index.json", - apps: apps, - count: count, - page_size: params.page_size, - admin: true - ) - end - end - - def create(%{body_params: params} = conn, _) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - case App.create(params) do - {:ok, app} -> - render(conn, "show.json", app: app, admin: true) - - {:error, changeset} -> - json(conn, App.errors(changeset)) - end - end - - def update(%{body_params: params} = conn, %{id: id}) do - params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) - - with {:ok, app} <- App.update(id, params) do - render(conn, "show.json", app: app, admin: true) - else - {:error, changeset} -> - json(conn, App.errors(changeset)) - - nil -> - json_response(conn, :bad_request, "") - end - end - - def delete(conn, params) do - with {:ok, _app} <- App.destroy(params.id) do - json_response(conn, :no_content, "") - else - _ -> json_response(conn, :bad_request, "") - end - end -end diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex new file mode 100644 index 000000000..a75f3e622 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex @@ -0,0 +1,217 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + summary: "List OAuth apps", + tags: ["Admin", "oAuth Apps"], + operationId: "AdminAPI.OAuthAppController.index", + security: [%{"oAuth" => ["write"]}], + parameters: [ + Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), + Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), + Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), + Operation.parameter( + :trusted, + :query, + %Schema{type: :boolean, default: false}, + "Trusted apps" + ), + Operation.parameter( + :page_size, + :query, + %Schema{type: :integer, default: 50}, + "Number of apps to return" + ) + | admin_api_params() + ], + responses: %{ + 200 => + Operation.response("List of apps", "application/json", %Schema{ + type: :object, + properties: %{ + apps: %Schema{type: :array, items: oauth_app()}, + count: %Schema{type: :integer}, + page_size: %Schema{type: :integer} + }, + example: %{ + "apps" => [ + %{ + "id" => 1, + "name" => "App name", + "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", + "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", + "redirect_uri" => "https://example.com/oauth-callback", + "website" => "https://example.com", + "trusted" => true + } + ], + "count" => 1, + "page_size" => 50 + } + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Create OAuth App", + operationId: "AdminAPI.OAuthAppController.create", + requestBody: request_body("Parameters", create_request()), + parameters: admin_api_params(), + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Update OAuth App", + operationId: "AdminAPI.OAuthAppController.update", + parameters: [id_param() | admin_api_params()], + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request()), + responses: %{ + 200 => Operation.response("App", "application/json", oauth_app()), + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + oneOf: [ApiError, %Schema{type: :string}] + }) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Admin", "oAuth Apps"], + summary: "Delete OAuth App", + operationId: "AdminAPI.OAuthAppController.delete", + parameters: [id_param() | admin_api_params()], + security: [%{"oAuth" => ["write"]}], + responses: %{ + 204 => no_content_response(), + 400 => no_content_response() + } + } + end + + defp create_request do + %Schema{ + title: "oAuthAppCreateRequest", + type: :object, + required: [:name, :redirect_uris], + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp update_request do + %Schema{ + title: "oAuthAppUpdateRequest", + type: :object, + properties: %{ + name: %Schema{type: :string, description: "Application Name"}, + scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, + redirect_uris: %Schema{ + type: :string, + description: + "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." + }, + website: %Schema{ + type: :string, + nullable: true, + description: "A URL to the homepage of the app" + }, + trusted: %Schema{ + type: :boolean, + nullable: true, + default: false, + description: "Is the app trusted?" + } + }, + example: %{ + "name" => "My App", + "redirect_uris" => "https://myapp.com/auth/callback", + "website" => "https://myapp.com/", + "scopes" => ["read", "write"], + "trusted" => true + } + } + end + + defp oauth_app do + %Schema{ + title: "oAuthApp", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + name: %Schema{type: :string}, + client_id: %Schema{type: :string}, + client_secret: %Schema{type: :string}, + redirect_uri: %Schema{type: :string}, + website: %Schema{type: :string, nullable: true}, + trusted: %Schema{type: :boolean} + }, + example: %{ + "id" => 123, + "name" => "My App", + "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", + "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", + "redirect_uri" => "https://myapp.com/oauth-callback", + "website" => "https://myapp.com/", + "trusted" => false + } + } + end + + def id_param do + Operation.parameter(:id, :path, :integer, "App ID", + example: 1337, + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex deleted file mode 100644 index a75f3e622..000000000 --- a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex +++ /dev/null @@ -1,217 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do - alias OpenApiSpex.Operation - alias OpenApiSpex.Schema - alias Pleroma.Web.ApiSpec.Schemas.ApiError - - import Pleroma.Web.ApiSpec.Helpers - - def open_api_operation(action) do - operation = String.to_existing_atom("#{action}_operation") - apply(__MODULE__, operation, []) - end - - def index_operation do - %Operation{ - summary: "List OAuth apps", - tags: ["Admin", "oAuth Apps"], - operationId: "AdminAPI.OAuthAppController.index", - security: [%{"oAuth" => ["write"]}], - parameters: [ - Operation.parameter(:name, :query, %Schema{type: :string}, "App name"), - Operation.parameter(:client_id, :query, %Schema{type: :string}, "Client ID"), - Operation.parameter(:page, :query, %Schema{type: :integer, default: 1}, "Page"), - Operation.parameter( - :trusted, - :query, - %Schema{type: :boolean, default: false}, - "Trusted apps" - ), - Operation.parameter( - :page_size, - :query, - %Schema{type: :integer, default: 50}, - "Number of apps to return" - ) - | admin_api_params() - ], - responses: %{ - 200 => - Operation.response("List of apps", "application/json", %Schema{ - type: :object, - properties: %{ - apps: %Schema{type: :array, items: oauth_app()}, - count: %Schema{type: :integer}, - page_size: %Schema{type: :integer} - }, - example: %{ - "apps" => [ - %{ - "id" => 1, - "name" => "App name", - "client_id" => "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk", - "client_secret" => "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY", - "redirect_uri" => "https://example.com/oauth-callback", - "website" => "https://example.com", - "trusted" => true - } - ], - "count" => 1, - "page_size" => 50 - } - }) - } - } - end - - def create_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Create OAuth App", - operationId: "AdminAPI.OAuthAppController.create", - requestBody: request_body("Parameters", create_request()), - parameters: admin_api_params(), - security: [%{"oAuth" => ["write"]}], - responses: %{ - 200 => Operation.response("App", "application/json", oauth_app()), - 400 => Operation.response("Bad Request", "application/json", ApiError) - } - } - end - - def update_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Update OAuth App", - operationId: "AdminAPI.OAuthAppController.update", - parameters: [id_param() | admin_api_params()], - security: [%{"oAuth" => ["write"]}], - requestBody: request_body("Parameters", update_request()), - responses: %{ - 200 => Operation.response("App", "application/json", oauth_app()), - 400 => - Operation.response("Bad Request", "application/json", %Schema{ - oneOf: [ApiError, %Schema{type: :string}] - }) - } - } - end - - def delete_operation do - %Operation{ - tags: ["Admin", "oAuth Apps"], - summary: "Delete OAuth App", - operationId: "AdminAPI.OAuthAppController.delete", - parameters: [id_param() | admin_api_params()], - security: [%{"oAuth" => ["write"]}], - responses: %{ - 204 => no_content_response(), - 400 => no_content_response() - } - } - end - - defp create_request do - %Schema{ - title: "oAuthAppCreateRequest", - type: :object, - required: [:name, :redirect_uris], - properties: %{ - name: %Schema{type: :string, description: "Application Name"}, - scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, - redirect_uris: %Schema{ - type: :string, - description: - "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." - }, - website: %Schema{ - type: :string, - nullable: true, - description: "A URL to the homepage of the app" - }, - trusted: %Schema{ - type: :boolean, - nullable: true, - default: false, - description: "Is the app trusted?" - } - }, - example: %{ - "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", - "website" => "https://myapp.com/", - "scopes" => ["read", "write"], - "trusted" => true - } - } - end - - defp update_request do - %Schema{ - title: "oAuthAppUpdateRequest", - type: :object, - properties: %{ - name: %Schema{type: :string, description: "Application Name"}, - scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"}, - redirect_uris: %Schema{ - type: :string, - description: - "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter." - }, - website: %Schema{ - type: :string, - nullable: true, - description: "A URL to the homepage of the app" - }, - trusted: %Schema{ - type: :boolean, - nullable: true, - default: false, - description: "Is the app trusted?" - } - }, - example: %{ - "name" => "My App", - "redirect_uris" => "https://myapp.com/auth/callback", - "website" => "https://myapp.com/", - "scopes" => ["read", "write"], - "trusted" => true - } - } - end - - defp oauth_app do - %Schema{ - title: "oAuthApp", - type: :object, - properties: %{ - id: %Schema{type: :integer}, - name: %Schema{type: :string}, - client_id: %Schema{type: :string}, - client_secret: %Schema{type: :string}, - redirect_uri: %Schema{type: :string}, - website: %Schema{type: :string, nullable: true}, - trusted: %Schema{type: :boolean} - }, - example: %{ - "id" => 123, - "name" => "My App", - "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM", - "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw", - "redirect_uri" => "https://myapp.com/oauth-callback", - "website" => "https://myapp.com/", - "trusted" => false - } - } - end - - def id_param do - Operation.parameter(:id, :path, :integer, "App ID", - example: 1337, - required: true - ) - end -end diff --git a/lib/pleroma/web/o_auth.ex b/lib/pleroma/web/o_auth.ex new file mode 100644 index 000000000..2f1b8708d --- /dev/null +++ b/lib/pleroma/web/o_auth.ex @@ -0,0 +1,6 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth do +end diff --git a/lib/pleroma/web/o_auth/app.ex b/lib/pleroma/web/o_auth/app.ex new file mode 100644 index 000000000..df99472e1 --- /dev/null +++ b/lib/pleroma/web/o_auth/app.ex @@ -0,0 +1,149 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.App do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + schema "apps" do + field(:client_name, :string) + field(:redirect_uris, :string) + field(:scopes, {:array, :string}, default: []) + field(:website, :string) + field(:client_id, :string) + field(:client_secret, :string) + field(:trusted, :boolean, default: false) + + has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) + has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) + + timestamps() + end + + @spec changeset(t(), map()) :: Ecto.Changeset.t() + def changeset(struct, params) do + cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) + end + + @spec register_changeset(t(), map()) :: Ecto.Changeset.t() + def register_changeset(struct, params \\ %{}) do + changeset = + struct + |> changeset(params) + |> validate_required([:client_name, :redirect_uris, :scopes]) + + if changeset.valid? do + changeset + |> put_change( + :client_id, + :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + ) + |> put_change( + :client_secret, + :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + ) + else + changeset + end + end + + @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def create(params) do + %__MODULE__{} + |> register_changeset(params) + |> Repo.insert() + end + + @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def update(id, params) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + app + |> changeset(params) + |> Repo.update() + end + end + + @doc """ + Gets app by attrs or create new with attrs. + And updates the scopes if need. + """ + @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def get_or_make(attrs, scopes) do + with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do + update_scopes(app, scopes) + else + _e -> + %__MODULE__{} + |> register_changeset(Map.put(attrs, :scopes, scopes)) + |> Repo.insert() + end + end + + defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app} + defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app} + + defp update_scopes(%__MODULE__{} = app, scopes) do + app + |> change(%{scopes: scopes}) + |> Repo.update() + end + + @spec search(map()) :: {:ok, [t()], non_neg_integer()} + def search(params) do + query = from(a in __MODULE__) + + query = + if params[:client_name] do + from(a in query, where: a.client_name == ^params[:client_name]) + else + query + end + + query = + if params[:client_id] do + from(a in query, where: a.client_id == ^params[:client_id]) + else + query + end + + query = + if Map.has_key?(params, :trusted) do + from(a in query, where: a.trusted == ^params[:trusted]) + else + query + end + + query = + from(u in query, + limit: ^params[:page_size], + offset: ^((params[:page] - 1) * params[:page_size]) + ) + + count = Repo.aggregate(__MODULE__, :count, :id) + + {:ok, Repo.all(query), count} + end + + @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} + def destroy(id) do + with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do + Repo.delete(app) + end + end + + @spec errors(Ecto.Changeset.t()) :: map() + def errors(changeset) do + Enum.reduce(changeset.errors, %{}, fn + {:client_name, {error, _}}, acc -> + Map.put(acc, :name, error) + + {key, {error, _}}, acc -> + Map.put(acc, key, error) + end) + end +end diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex new file mode 100644 index 000000000..268ee5b63 --- /dev/null +++ b/lib/pleroma/web/o_auth/authorization.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Authorization do + use Ecto.Schema + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + + import Ecto.Changeset + import Ecto.Query + + @type t :: %__MODULE__{} + + schema "oauth_authorizations" do + field(:token, :string) + field(:scopes, {:array, :string}, default: []) + field(:valid_until, :naive_datetime_usec) + field(:used, :boolean, default: false) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:app, App) + + timestamps() + end + + @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} + def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do + %{ + scopes: scopes || app.scopes, + user_id: user.id, + app_id: app.id + } + |> create_changeset() + |> Repo.insert() + end + + @spec create_changeset(map()) :: Changeset.t() + def create_changeset(attrs \\ %{}) do + %Authorization{} + |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) + |> validate_required([:app_id, :scopes]) + |> add_token() + |> add_lifetime() + end + + defp add_token(changeset) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + put_change(changeset, :token, token) + end + + defp add_lifetime(changeset) do + put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) + end + + @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() + def use_changeset(%Authorization{} = auth, params) do + auth + |> cast(params, [:used]) + |> validate_required([:used]) + end + + @spec use_token(Authorization.t()) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} + def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do + if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do + Repo.update(use_changeset(auth, %{used: true})) + else + {:error, "token expired"} + end + end + + def use_token(%Authorization{used: true}), do: {:error, "already used"} + + @spec delete_user_authorizations(User.t()) :: {integer(), any()} + 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 + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end +end diff --git a/lib/pleroma/web/o_auth/fallback_controller.ex b/lib/pleroma/web/o_auth/fallback_controller.ex new file mode 100644 index 000000000..a89ced886 --- /dev/null +++ b/lib/pleroma/web/o_auth/fallback_controller.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.FallbackController do + use Pleroma.Web, :controller + alias Pleroma.Web.OAuth.OAuthController + + def call(conn, {:register, :generic_error}) do + conn + |> put_status(:internal_server_error) + |> 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, dgettext("errors", "Invalid Username/Password")) + |> OAuthController.registration_details(conn.params) + end + + def call(conn, _error) do + conn + |> put_status(:unauthorized) + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) + |> OAuthController.authorize(conn.params) + end +end diff --git a/lib/pleroma/web/o_auth/mfa_controller.ex b/lib/pleroma/web/o_auth/mfa_controller.ex new file mode 100644 index 000000000..f102c93e7 --- /dev/null +++ b/lib/pleroma/web/o_auth/mfa_controller.ex @@ -0,0 +1,98 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAController do + @moduledoc """ + The model represents api to use Multi Factor authentications. + """ + + use Pleroma.Web, :controller + + alias Pleroma.MFA + alias Pleroma.Web.Auth.TOTPAuthenticator + alias Pleroma.Web.OAuth.MFAView, as: View + alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Token + + plug(:fetch_session when action in [:show, :verify]) + plug(:fetch_flash when action in [:show, :verify]) + + @doc """ + Display form to input mfa code or recovery code. + """ + def show(conn, %{"mfa_token" => mfa_token} = params) do + template = Map.get(params, "challenge_type", "totp") + + conn + |> put_view(View) + |> render("#{template}.html", %{ + mfa_token: mfa_token, + redirect_uri: params["redirect_uri"], + state: params["state"] + }) + end + + @doc """ + Verification code and continue authorization. + """ + def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do + with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), + {:ok, _} <- validates_challenge(user, mfa_params) do + conn + |> OAuthController.after_create_authorization(auth, %{ + "authorization" => %{ + "redirect_uri" => mfa_params["redirect_uri"], + "state" => mfa_params["state"] + } + }) + else + _ -> + conn + |> put_flash(:error, "Two-factor authentication failed.") + |> put_status(:unauthorized) + |> show(mfa_params) + end + end + + @doc """ + Verification second step of MFA (or recovery) and returns access token. + + ## Endpoint + POST /oauth/mfa/challenge + + params: + `client_id` + `client_secret` + `mfa_token` - access token to check second step of mfa + `challenge_type` - 'totp' or 'recovery' + `code` + + """ + def challenge(conn, %{"mfa_token" => mfa_token} = params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), + {:ok, _} <- validates_challenge(user, params), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> + conn + |> put_status(400) + |> json(%{error: "Invalid code"}) + end + end + + # Verify TOTP Code + defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do + TOTPAuthenticator.verify(code, user) + end + + # Verify Recovery Code + defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do + TOTPAuthenticator.verify_recovery_code(user, code) + end + + defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type} +end diff --git a/lib/pleroma/web/o_auth/mfa_view.ex b/lib/pleroma/web/o_auth/mfa_view.ex new file mode 100644 index 000000000..5d87db268 --- /dev/null +++ b/lib/pleroma/web/o_auth/mfa_view.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAView do + use Pleroma.Web, :view + import Phoenix.HTML.Form + alias Pleroma.MFA + + def render("mfa_response.json", %{token: token, user: user}) do + %{ + error: "mfa_required", + mfa_token: token.token, + supported_challenge_types: MFA.supported_methods(user) + } + end +end diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex new file mode 100644 index 000000000..a4152e840 --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -0,0 +1,610 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthController do + use Pleroma.Web, :controller + + alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps + alias Pleroma.MFA + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Registration + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.Auth.Authenticator + alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView + alias Pleroma.Web.OAuth.Scopes + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken + alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken + + require Logger + + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + + plug(:fetch_session) + plug(:fetch_flash) + + plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) + + plug(RateLimiter, [name: :authentication] when action == :create_authorization) + + 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(%Plug.Conn{} = conn, %{"authorization" => _} = params) do + {auth_attrs, params} = Map.pop(params, "authorization") + authorize(conn, Map.merge(params, auth_attrs)) + end + + 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 + handle_existing_authorization(conn, params) + 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 + app = Repo.get_by(App, client_id: params["client_id"]) + available_scopes = (app && app.scopes) || [] + scopes = Scopes.fetch_scopes(params, available_scopes) + + scopes = + if scopes == [] do + available_scopes + else + scopes + end + + # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template + render(conn, Authenticator.auth_template(), %{ + response_type: params["response_type"], + client_id: params["client_id"], + available_scopes: available_scopes, + scopes: scopes, + redirect_uri: params["redirect_uri"], + state: params["state"], + params: params + }) + 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 = Maps.put_if_present(url_params, :state, params["state"]) + url = UriHelper.modify_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( + %Plug.Conn{} = conn, + %{"authorization" => _} = params, + opts \\ [] + ) do + with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do + after_create_authorization(conn, auth, params) + else + error -> + handle_create_authorization_error(conn, error, params) + end + end + + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} + }) do + # Enforcing the view to reuse the template when calling from other controllers + conn + |> put_view(OAuthView) + |> render("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 = Maps.put_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.modify_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( + %Plug.Conn{} = conn, + {:error, scopes_issue}, + %{"authorization" => _} = params + ) + when scopes_issue in [:unsupported_scopes, :missing_scopes] do + # Per https://github.com/tootsuite/mastodon/blob/ + # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 + conn + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) + |> put_status(:unauthorized) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :confirmation_pending}, + %{"authorization" => _} = params + ) do + conn + |> 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( + %Plug.Conn{} = conn, + {:mfa_required, user, auth, _}, + params + ) do + {:ok, token} = MFA.Token.create(user, auth) + + data = %{ + "mfa_token" => token.token, + "redirect_uri" => params["authorization"]["redirect_uri"], + "state" => params["authorization"]["state"] + } + + MFAController.show(conn, data) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Password reset is required")) + |> put_status(:forbidden) + |> authorize(params) + end + + defp handle_create_authorization_error( + %Plug.Conn{} = conn, + {:account_status, :deactivated}, + %{"authorization" => _} = params + ) do + conn + |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) + |> put_status(:forbidden) + |> authorize(params) + end + + 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( + %Plug.Conn{} = conn, + %{"grant_type" => "refresh_token", "refresh_token" => token} = _params + ) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), + {:ok, token} <- RefreshToken.grant(token) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + _error -> render_invalid_credentials_error(conn) + end + end + + 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), + %User{} = user <- User.get_cached_by_id(auth.user_id), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password"} = params + ) do + with {:ok, %User{} = user} <- Authenticator.get_user(conn), + {:ok, app} <- Token.Utils.fetch_app(conn), + requested_scopes <- Scopes.fetch_scopes(params, app.scopes), + {:ok, token} <- login(user, app, requested_scopes) do + json(conn, OAuthView.render("token.json", %{user: user, token: token})) + else + error -> + handle_token_exchange_error(conn, error) + end + end + + def token_exchange( + %Plug.Conn{} = conn, + %{"grant_type" => "password", "name" => name, "password" => _password} = params + ) do + params = + params + |> Map.delete("name") + |> Map.put("username", name) + + token_exchange(conn, params) + end + + 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, OAuthView.render("token.json", %{token: token})) + else + _error -> + handle_token_exchange_error(conn, :invalid_credentails) + end + end + + # Bad request + def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do + conn + |> put_status(:forbidden) + |> json(build_and_response_mfa_token(user, auth)) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do + render_error( + conn, + :forbidden, + "Your account is currently disabled", + %{}, + "account_is_disabled" + ) + end + + defp handle_token_exchange_error( + %Plug.Conn{} = conn, + {:account_status, :password_reset_pending} + ) do + render_error( + conn, + :forbidden, + "Password reset is required", + %{}, + "password_reset_required" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do + render_error( + conn, + :forbidden, + "Your login is missing a confirmed e-mail address", + %{}, + "missing_confirmed_email" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do + render_error( + conn, + :forbidden, + "Your account is awaiting approval.", + %{}, + "awaiting_approval" + ) + end + + defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do + render_invalid_credentials_error(conn) + end + + 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, %{}) + else + _error -> + # RFC 7009: invalid tokens [in the request] do not cause an error response + json(conn, %{}) + end + end + + def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) + + # Response for 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(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do + scope = + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() + + state = + auth_attrs + |> Map.delete("scopes") + |> Map.put("scope", scope) + |> Jason.encode!() + + params = + auth_attrs + |> Map.drop(~w(scope scopes client_id redirect_uri)) + |> Map.put("state", state) + + # Handing the request to Ueberauth + redirect(conn, to: o_auth_path(conn, :request, provider, params)) + end + + def request(%Plug.Conn{} = conn, params) do + message = + if params["provider"] do + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) + else + dgettext("errors", "Bad OAuth request.") + end + + conn + |> put_flash(:error, message) + |> redirect(to: "/") + end + + 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, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + + def callback(%Plug.Conn{} = conn, params) do + params = callback_params(params) + + with {:ok, registration} <- Authenticator.get_registration(conn) do + auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) + + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + create_authorization(conn, %{"authorization" => auth_attrs}, user: user) + + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> 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, dgettext("errors", "Failed to set up user account.")) + |> redirect(external: redirect_uri(conn, params["redirect_uri"])) + end + end + + defp callback_params(%{"state" => state} = params) do + Map.merge(params, Jason.decode!(state)) + end + + 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"], + state: auth_attrs["state"], + scopes: Scopes.fetch_scopes(auth_attrs, []), + nickname: auth_attrs["nickname"], + email: auth_attrs["email"] + }) + end + + 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, _user}} <- + {:create_authorization, do_create_authorization(conn, params)}, + %User{} = user <- Repo.preload(auth, :user).user, + {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do + conn + |> put_session_registration_id(nil) + |> after_create_authorization(auth, params) + else + {:create_authorization, error} -> + {:register, handle_create_authorization_error(conn, error, params)} + + _ -> + {:register, :generic_error} + end + end + + 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 + conn + |> put_session_registration_id(nil) + |> create_authorization( + params, + user: user + ) + else + {:error, changeset} -> + message = + Enum.map(changeset.errors, fn {field, {error, _}} -> + "#{field} #{error}" + end) + |> Enum.join("; ") + + message = + String.replace( + message, + "ap_id has already been taken", + "nickname has already been taken" + ) + + conn + |> put_status(:forbidden) + |> put_flash(:error, "Error: #{message}.") + |> registration_details(params) + + _ -> + {:register, :generic_error} + end + end + + defp do_create_authorization(conn, auth_attrs, user \\ nil) + + defp do_create_authorization( + %Plug.Conn{} = conn, + %{ + "authorization" => + %{ + "client_id" => client_id, + "redirect_uri" => redirect_uri + } = auth_attrs + }, + user + ) do + with {_, {:ok, %User{} = user}} <- + {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, + %App{} = app <- Repo.get_by(App, client_id: client_id), + true <- redirect_uri in String.split(app.redirect_uris), + requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), + {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do + {:ok, auth, user} + end + end + + defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) + when is_list(requested_scopes) do + with {:account_status, :active} <- {:account_status, User.account_status(user)}, + {:ok, scopes} <- validate_scopes(app, requested_scopes), + {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do + {:ok, auth} + end + end + + # Note: intended to be a private function but opened for AccountController that logs in on signup + @doc "If checks pass, creates authorization and token for given user, app and requested scopes." + def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do + with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), + {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, + {:ok, token} <- Token.exchange_token(app, auth) do + {:ok, token} + end + end + + # Special case: Local MastodonFE + defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) + + defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri + + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) + + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), + do: put_session(conn, :registration_id, registration_id) + + defp build_and_response_mfa_token(user, auth) do + with {:ok, token} <- MFA.Token.create(user, auth) do + MFAView.render("mfa_response.json", %{token: token, user: user}) + end + end + + @spec validate_scopes(App.t(), map() | list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + defp validate_scopes(%App{} = app, params) when is_map(params) do + requested_scopes = Scopes.fetch_scopes(params, app.scopes) + validate_scopes(app, requested_scopes) + end + + defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do + Scopes.validate(requested_scopes, 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/o_auth/o_auth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex new file mode 100644 index 000000000..f55247ebd --- /dev/null +++ b/lib/pleroma/web/o_auth/o_auth_view.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.OAuthView do + use Pleroma.Web, :view + import Phoenix.HTML.Form + + alias Pleroma.Web.OAuth.Token.Utils + + def render("token.json", %{token: token} = opts) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: expires_in(), + scope: Enum.join(token.scopes, " "), + created_at: Utils.format_created_at(token) + } + + if user = opts[:user] do + response + |> Map.put(:me, user.ap_id) + else + response + end + end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) +end diff --git a/lib/pleroma/web/o_auth/scopes.ex b/lib/pleroma/web/o_auth/scopes.ex new file mode 100644 index 000000000..6f06f1431 --- /dev/null +++ b/lib/pleroma/web/o_auth/scopes.ex @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Scopes do + @moduledoc """ + Functions for dealing with scopes. + """ + + alias Pleroma.Plugs.OAuthScopesPlug + + @doc """ + Fetch scopes from request params. + + Note: `scopes` is used by Mastodon — supporting it but sticking to + OAuth's standard `scope` wherever we control it + """ + @spec fetch_scopes(map() | struct(), list()) :: list() + + def fetch_scopes(params, default) do + parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) + end + + def parse_scopes(scopes, _default) when is_list(scopes) do + Enum.filter(scopes, &(&1 not in [nil, ""])) + end + + def parse_scopes(scopes, default) when is_binary(scopes) do + scopes + |> to_list + |> parse_scopes(default) + end + + def parse_scopes(_, default) do + default + end + + @doc """ + Convert scopes string to list + """ + @spec to_list(binary()) :: [binary()] + def to_list(nil), do: [] + + def to_list(str) do + str + |> String.trim() + |> String.split(~r/[\s,]+/) + end + + @doc """ + Convert scopes list to string + """ + @spec to_string(list()) :: binary() + def to_string(scopes), do: Enum.join(scopes, " ") + + @doc """ + Validates scopes. + """ + @spec validate(list() | nil, list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} + + def validate(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + ^scopes -> {:ok, scopes} + _ -> {:error, :unsupported_scopes} + end + end + + def contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end +end diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex new file mode 100644 index 000000000..de37998f2 --- /dev/null +++ b/lib/pleroma/web/o_auth/token.ex @@ -0,0 +1,135 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token do + use Ecto.Schema + + import Ecto.Changeset + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Query + + @type t :: %__MODULE__{} + + schema "oauth_tokens" do + field(:token, :string) + field(:refresh_token, :string) + field(:scopes, {:array, :string}, default: []) + field(:valid_until, :naive_datetime_usec) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:app, App) + + timestamps() + end + + @doc "Gets token for app by access 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 + Query.get_by_app(app_id) + |> Query.get_by_token(token) + |> Repo.find_resource() + end + + @doc "Gets token for app by refresh token" + @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_refresh_token(%App{id: app_id} = _app, token) do + Query.get_by_app(app_id) + |> Query.get_by_refresh_token(token) + |> Query.preload([:user]) + |> Repo.find_resource() + end + + @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} + def exchange_token(app, auth) do + with {:ok, auth} <- Authorization.use_token(auth), + true <- auth.app_id == app.id do + user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} + + create( + app, + user, + %{scopes: auth.scopes} + ) + end + end + + defp put_token(changeset) do + changeset + |> change(%{token: Token.Utils.generate_token()}) + |> validate_required([:token]) + |> unique_constraint(:token) + end + + defp put_refresh_token(changeset, attrs) do + refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) + + changeset + |> change(%{refresh_token: refresh_token}) + |> validate_required([:refresh_token]) + |> unique_constraint(:refresh_token) + end + + defp put_valid_until(changeset, attrs) do + expires_in = + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) + + changeset + |> change(%{valid_until: expires_in}) + |> validate_required([:valid_until]) + end + + @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} + def create(%App{} = app, %User{} = user, attrs \\ %{}) do + with {:ok, token} <- do_create(app, user, attrs) do + if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do + Pleroma.Workers.PurgeExpiredToken.enqueue(%{ + token_id: token.id, + valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), + mod: __MODULE__ + }) + end + + {:ok, token} + end + end + + defp do_create(app, user, attrs) do + %__MODULE__{user_id: user.id, app_id: app.id} + |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) + |> validate_required([:scopes, :app_id]) + |> put_valid_until(attrs) + |> put_token() + |> put_refresh_token(attrs) + |> Repo.insert() + end + + def delete_user_tokens(%User{id: user_id}) do + Query.get_by_user(user_id) + |> Repo.delete_all() + end + + def delete_user_token(%User{id: user_id}, token_id) do + Query.get_by_user(user_id) + |> Query.get_by_id(token_id) + |> Repo.delete_all() + end + + def get_user_tokens(%User{id: user_id}) do + Query.get_by_user(user_id) + |> Query.preload([:app]) + |> Repo.all() + end + + def is_expired?(%__MODULE__{valid_until: valid_until}) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 + 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/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex new file mode 100644 index 000000000..fd6d9b112 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/query.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Query do + @moduledoc """ + Contains queries for OAuth Token. + """ + + import Ecto.Query, only: [from: 2] + + @type query :: Ecto.Queryable.t() | Token.t() + + alias Pleroma.Web.OAuth.Token + + @spec get_by_refresh_token(query, String.t()) :: query + def get_by_refresh_token(query \\ Token, refresh_token) do + from(q in query, where: q.refresh_token == ^refresh_token) + end + + @spec get_by_token(query, String.t()) :: query + def get_by_token(query \\ Token, token) do + from(q in query, where: q.token == ^token) + end + + @spec get_by_app(query, String.t()) :: query + def get_by_app(query \\ Token, app_id) do + from(q in query, where: q.app_id == ^app_id) + end + + @spec get_by_id(query, String.t()) :: query + def get_by_id(query \\ Token, id) do + from(q in query, where: q.id == ^id) + end + + @spec get_by_user(query, String.t()) :: query + def get_by_user(query \\ Token, user_id) do + from(q in query, where: q.user_id == ^user_id) + end + + @spec preload(query, any) :: query + def preload(query \\ Token, assoc_preload \\ []) + + def preload(query, assoc_preload) when is_list(assoc_preload) do + from(q in query, preload: ^assoc_preload) + end + + def preload(query, _assoc_preload), do: query +end diff --git a/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex new file mode 100644 index 000000000..625b0fde2 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/strategy/refresh_token.ex @@ -0,0 +1,58 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do + @moduledoc """ + Functions for dealing with refresh token strategy. + """ + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.Revoke + + @doc """ + Will grant access token by refresh token. + """ + @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} + def grant(token) do + access_token = Repo.preload(token, [:user, :app]) + + result = + Repo.transaction(fn -> + token_params = %{ + app: access_token.app, + user: access_token.user, + scopes: access_token.scopes + } + + access_token + |> revoke_access_token() + |> create_access_token(token_params) + end) + + case result do + {:ok, {:error, reason}} -> {:error, reason} + {:ok, {:ok, token}} -> {:ok, token} + {:error, reason} -> {:error, reason} + end + end + + defp revoke_access_token(token) do + Revoke.revoke(token) + end + + defp create_access_token({:error, error}, _), do: {:error, error} + + defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do + Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) + end + + defp add_refresh_token(params, token) do + case Config.get([:oauth2, :issue_new_refresh_token], false) do + true -> Map.put(params, :refresh_token, token) + false -> params + end + end +end diff --git a/lib/pleroma/web/o_auth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex new file mode 100644 index 000000000..069c1ee21 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do + @moduledoc """ + Functions for dealing with revocation. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @doc "Finds and revokes access token for app and by token" + @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} + def revoke(%App{} = app, %{"token" => token} = _attrs) do + with {:ok, token} <- Token.get_by_token(app, token), + do: revoke(token) + end + + @doc "Revokes access token" + @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} + def revoke(%Token{} = token) do + Repo.delete(token) + end +end diff --git a/lib/pleroma/web/o_auth/token/utils.ex b/lib/pleroma/web/o_auth/token/utils.ex new file mode 100644 index 000000000..43aeab6b0 --- /dev/null +++ b/lib/pleroma/web/o_auth/token/utils.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.Utils do + @moduledoc """ + Auxiliary functions for dealing with tokens. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + + @doc "Fetch app by client credentials from request" + @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} + def fetch_app(conn) do + res = + conn + |> fetch_client_credentials() + |> fetch_client + + case res do + %App{} = app -> {:ok, app} + _ -> {:error, :not_found} + end + end + + defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do + Repo.get_by(App, client_id: id, client_secret: secret) + end + + defp fetch_client({_id, _secret}), do: nil + + defp fetch_client_credentials(conn) do + # Per RFC 6749, HTTP Basic is preferred to body params + with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), + {:ok, decoded} <- Base.decode64(encoded), + [id, secret] <- + Enum.map( + String.split(decoded, ":"), + fn s -> URI.decode_www_form(s) end + ) do + {id, secret} + else + _ -> {conn.params["client_id"], conn.params["client_secret"]} + end + end + + @doc "convert token inserted_at to unix timestamp" + def format_created_at(%{inserted_at: inserted_at} = _token) do + inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + end + + @doc false + @spec generate_token(keyword()) :: binary() + def generate_token(opts \\ []) do + opts + |> Keyword.get(:size, 32) + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end + + # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be + # decoding it. Investigate sometime. + def fix_padding(token) do + token + |> URI.decode() + |> Base.url_decode64!(padding: false) + |> Base.url_encode64(padding: false) + end +end diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex deleted file mode 100644 index 2f1b8708d..000000000 --- a/lib/pleroma/web/oauth.ex +++ /dev/null @@ -1,6 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth do -end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex deleted file mode 100644 index df99472e1..000000000 --- a/lib/pleroma/web/oauth/app.ex +++ /dev/null @@ -1,149 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.App do - use Ecto.Schema - import Ecto.Changeset - import Ecto.Query - alias Pleroma.Repo - - @type t :: %__MODULE__{} - - schema "apps" do - field(:client_name, :string) - field(:redirect_uris, :string) - field(:scopes, {:array, :string}, default: []) - field(:website, :string) - field(:client_id, :string) - field(:client_secret, :string) - field(:trusted, :boolean, default: false) - - has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all) - has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all) - - timestamps() - end - - @spec changeset(t(), map()) :: Ecto.Changeset.t() - def changeset(struct, params) do - cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted]) - end - - @spec register_changeset(t(), map()) :: Ecto.Changeset.t() - def register_changeset(struct, params \\ %{}) do - changeset = - struct - |> changeset(params) - |> validate_required([:client_name, :redirect_uris, :scopes]) - - if changeset.valid? do - changeset - |> put_change( - :client_id, - :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - ) - |> put_change( - :client_secret, - :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - ) - else - changeset - end - end - - @spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def create(params) do - %__MODULE__{} - |> register_changeset(params) - |> Repo.insert() - end - - @spec update(pos_integer(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def update(id, params) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do - app - |> changeset(params) - |> Repo.update() - end - end - - @doc """ - Gets app by attrs or create new with attrs. - And updates the scopes if need. - """ - @spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def get_or_make(attrs, scopes) do - with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do - update_scopes(app, scopes) - else - _e -> - %__MODULE__{} - |> register_changeset(Map.put(attrs, :scopes, scopes)) - |> Repo.insert() - end - end - - defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app} - defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app} - - defp update_scopes(%__MODULE__{} = app, scopes) do - app - |> change(%{scopes: scopes}) - |> Repo.update() - end - - @spec search(map()) :: {:ok, [t()], non_neg_integer()} - def search(params) do - query = from(a in __MODULE__) - - query = - if params[:client_name] do - from(a in query, where: a.client_name == ^params[:client_name]) - else - query - end - - query = - if params[:client_id] do - from(a in query, where: a.client_id == ^params[:client_id]) - else - query - end - - query = - if Map.has_key?(params, :trusted) do - from(a in query, where: a.trusted == ^params[:trusted]) - else - query - end - - query = - from(u in query, - limit: ^params[:page_size], - offset: ^((params[:page] - 1) * params[:page_size]) - ) - - count = Repo.aggregate(__MODULE__, :count, :id) - - {:ok, Repo.all(query), count} - end - - @spec destroy(pos_integer()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def destroy(id) do - with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do - Repo.delete(app) - end - end - - @spec errors(Ecto.Changeset.t()) :: map() - def errors(changeset) do - Enum.reduce(changeset.errors, %{}, fn - {:client_name, {error, _}}, acc -> - Map.put(acc, :name, error) - - {key, {error, _}}, acc -> - Map.put(acc, key, error) - end) - end -end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex deleted file mode 100644 index 268ee5b63..000000000 --- a/lib/pleroma/web/oauth/authorization.ex +++ /dev/null @@ -1,95 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Authorization do - use Ecto.Schema - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - - import Ecto.Changeset - import Ecto.Query - - @type t :: %__MODULE__{} - - schema "oauth_authorizations" do - field(:token, :string) - field(:scopes, {:array, :string}, default: []) - field(:valid_until, :naive_datetime_usec) - field(:used, :boolean, default: false) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:app, App) - - timestamps() - end - - @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: - {:ok, Authorization.t()} | {:error, Changeset.t()} - def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do - %{ - scopes: scopes || app.scopes, - user_id: user.id, - app_id: app.id - } - |> create_changeset() - |> Repo.insert() - end - - @spec create_changeset(map()) :: Changeset.t() - def create_changeset(attrs \\ %{}) do - %Authorization{} - |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) - |> validate_required([:app_id, :scopes]) - |> add_token() - |> add_lifetime() - end - - defp add_token(changeset) do - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - put_change(changeset, :token, token) - end - - defp add_lifetime(changeset) do - put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) - end - - @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() - def use_changeset(%Authorization{} = auth, params) do - auth - |> cast(params, [:used]) - |> validate_required([:used]) - end - - @spec use_token(Authorization.t()) :: - {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} - def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do - if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do - Repo.update(use_changeset(auth, %{used: true})) - else - {:error, "token expired"} - end - end - - def use_token(%Authorization{used: true}), do: {:error, "already used"} - - @spec delete_user_authorizations(User.t()) :: {integer(), any()} - 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 - from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) - |> Repo.find_resource() - end -end diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex deleted file mode 100644 index a89ced886..000000000 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.FallbackController do - use Pleroma.Web, :controller - alias Pleroma.Web.OAuth.OAuthController - - def call(conn, {:register, :generic_error}) do - conn - |> put_status(:internal_server_error) - |> 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, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.registration_details(conn.params) - end - - def call(conn, _error) do - conn - |> put_status(:unauthorized) - |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) - |> OAuthController.authorize(conn.params) - end -end diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex deleted file mode 100644 index f102c93e7..000000000 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ /dev/null @@ -1,98 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAController do - @moduledoc """ - The model represents api to use Multi Factor authentications. - """ - - use Pleroma.Web, :controller - - alias Pleroma.MFA - alias Pleroma.Web.Auth.TOTPAuthenticator - alias Pleroma.Web.OAuth.MFAView, as: View - alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Token - - plug(:fetch_session when action in [:show, :verify]) - plug(:fetch_flash when action in [:show, :verify]) - - @doc """ - Display form to input mfa code or recovery code. - """ - def show(conn, %{"mfa_token" => mfa_token} = params) do - template = Map.get(params, "challenge_type", "totp") - - conn - |> put_view(View) - |> render("#{template}.html", %{ - mfa_token: mfa_token, - redirect_uri: params["redirect_uri"], - state: params["state"] - }) - end - - @doc """ - Verification code and continue authorization. - """ - def verify(conn, %{"mfa" => %{"mfa_token" => mfa_token} = mfa_params} = _) do - with {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), - {:ok, _} <- validates_challenge(user, mfa_params) do - conn - |> OAuthController.after_create_authorization(auth, %{ - "authorization" => %{ - "redirect_uri" => mfa_params["redirect_uri"], - "state" => mfa_params["state"] - } - }) - else - _ -> - conn - |> put_flash(:error, "Two-factor authentication failed.") - |> put_status(:unauthorized) - |> show(mfa_params) - end - end - - @doc """ - Verification second step of MFA (or recovery) and returns access token. - - ## Endpoint - POST /oauth/mfa/challenge - - params: - `client_id` - `client_secret` - `mfa_token` - access token to check second step of mfa - `challenge_type` - 'totp' or 'recovery' - `code` - - """ - def challenge(conn, %{"mfa_token" => mfa_token} = params) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), - {:ok, _} <- validates_challenge(user, params), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> - conn - |> put_status(400) - |> json(%{error: "Invalid code"}) - end - end - - # Verify TOTP Code - defp validates_challenge(user, %{"challenge_type" => "totp", "code" => code} = _) do - TOTPAuthenticator.verify(code, user) - end - - # Verify Recovery Code - defp validates_challenge(user, %{"challenge_type" => "recovery", "code" => code} = _) do - TOTPAuthenticator.verify_recovery_code(user, code) - end - - defp validates_challenge(_, _), do: {:error, :unsupported_challenge_type} -end diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex deleted file mode 100644 index 5d87db268..000000000 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.MFAView do - use Pleroma.Web, :view - import Phoenix.HTML.Form - alias Pleroma.MFA - - def render("mfa_response.json", %{token: token, user: user}) do - %{ - error: "mfa_required", - mfa_token: token.token, - supported_challenge_types: MFA.supported_methods(user) - } - end -end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex deleted file mode 100644 index a4152e840..000000000 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ /dev/null @@ -1,610 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthController do - use Pleroma.Web, :controller - - alias Pleroma.Helpers.UriHelper - alias Pleroma.Maps - alias Pleroma.MFA - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Registration - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.Auth.Authenticator - alias Pleroma.Web.ControllerHelper - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.MFAController - alias Pleroma.Web.OAuth.MFAView - alias Pleroma.Web.OAuth.OAuthView - alias Pleroma.Web.OAuth.Scopes - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken - alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken - - require Logger - - if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) - - plug(:fetch_session) - plug(:fetch_flash) - - plug(:skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]) - - plug(RateLimiter, [name: :authentication] when action == :create_authorization) - - 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(%Plug.Conn{} = conn, %{"authorization" => _} = params) do - {auth_attrs, params} = Map.pop(params, "authorization") - authorize(conn, Map.merge(params, auth_attrs)) - end - - 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 - handle_existing_authorization(conn, params) - 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 - app = Repo.get_by(App, client_id: params["client_id"]) - available_scopes = (app && app.scopes) || [] - scopes = Scopes.fetch_scopes(params, available_scopes) - - scopes = - if scopes == [] do - available_scopes - else - scopes - end - - # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template - render(conn, Authenticator.auth_template(), %{ - response_type: params["response_type"], - client_id: params["client_id"], - available_scopes: available_scopes, - scopes: scopes, - redirect_uri: params["redirect_uri"], - state: params["state"], - params: params - }) - 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 = Maps.put_if_present(url_params, :state, params["state"]) - url = UriHelper.modify_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( - %Plug.Conn{} = conn, - %{"authorization" => _} = params, - opts \\ [] - ) do - with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do - after_create_authorization(conn, auth, params) - else - error -> - handle_create_authorization_error(conn, error, params) - end - end - - def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ - "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} - }) do - # Enforcing the view to reuse the template when calling from other controllers - conn - |> put_view(OAuthView) - |> render("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 = Maps.put_if_present(url_params, :state, auth_attrs["state"]) - url = UriHelper.modify_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( - %Plug.Conn{} = conn, - {:error, scopes_issue}, - %{"authorization" => _} = params - ) - when scopes_issue in [:unsupported_scopes, :missing_scopes] do - # Per https://github.com/tootsuite/mastodon/blob/ - # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 - conn - |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) - |> put_status(:unauthorized) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :confirmation_pending}, - %{"authorization" => _} = params - ) do - conn - |> 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( - %Plug.Conn{} = conn, - {:mfa_required, user, auth, _}, - params - ) do - {:ok, token} = MFA.Token.create(user, auth) - - data = %{ - "mfa_token" => token.token, - "redirect_uri" => params["authorization"]["redirect_uri"], - "state" => params["authorization"]["state"] - } - - MFAController.show(conn, data) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Password reset is required")) - |> put_status(:forbidden) - |> authorize(params) - end - - defp handle_create_authorization_error( - %Plug.Conn{} = conn, - {:account_status, :deactivated}, - %{"authorization" => _} = params - ) do - conn - |> put_flash(:error, dgettext("errors", "Your account is currently disabled")) - |> put_status(:forbidden) - |> authorize(params) - end - - 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( - %Plug.Conn{} = conn, - %{"grant_type" => "refresh_token", "refresh_token" => token} = _params - ) do - with {:ok, app} <- Token.Utils.fetch_app(conn), - {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), - {:ok, token} <- RefreshToken.grant(token) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - _error -> render_invalid_credentials_error(conn) - end - end - - 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), - %User{} = user <- User.get_cached_by_id(auth.user_id), - {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password"} = params - ) do - with {:ok, %User{} = user} <- Authenticator.get_user(conn), - {:ok, app} <- Token.Utils.fetch_app(conn), - requested_scopes <- Scopes.fetch_scopes(params, app.scopes), - {:ok, token} <- login(user, app, requested_scopes) do - json(conn, OAuthView.render("token.json", %{user: user, token: token})) - else - error -> - handle_token_exchange_error(conn, error) - end - end - - def token_exchange( - %Plug.Conn{} = conn, - %{"grant_type" => "password", "name" => name, "password" => _password} = params - ) do - params = - params - |> Map.delete("name") - |> Map.put("username", name) - - token_exchange(conn, params) - end - - 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, OAuthView.render("token.json", %{token: token})) - else - _error -> - handle_token_exchange_error(conn, :invalid_credentails) - end - end - - # Bad request - def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:mfa_required, user, auth, _}) do - conn - |> put_status(:forbidden) - |> json(build_and_response_mfa_token(user, auth)) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :deactivated}) do - render_error( - conn, - :forbidden, - "Your account is currently disabled", - %{}, - "account_is_disabled" - ) - end - - defp handle_token_exchange_error( - %Plug.Conn{} = conn, - {:account_status, :password_reset_pending} - ) do - render_error( - conn, - :forbidden, - "Password reset is required", - %{}, - "password_reset_required" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirmation_pending}) do - render_error( - conn, - :forbidden, - "Your login is missing a confirmed e-mail address", - %{}, - "missing_confirmed_email" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do - render_error( - conn, - :forbidden, - "Your account is awaiting approval.", - %{}, - "awaiting_approval" - ) - end - - defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do - render_invalid_credentials_error(conn) - end - - 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, %{}) - else - _error -> - # RFC 7009: invalid tokens [in the request] do not cause an error response - json(conn, %{}) - end - end - - def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - - # Response for 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(%Plug.Conn{} = conn, %{ - "provider" => provider, - "authorization" => auth_attrs - }) do - scope = - auth_attrs - |> Scopes.fetch_scopes([]) - |> Scopes.to_string() - - state = - auth_attrs - |> Map.delete("scopes") - |> Map.put("scope", scope) - |> Jason.encode!() - - params = - auth_attrs - |> Map.drop(~w(scope scopes client_id redirect_uri)) - |> Map.put("state", state) - - # Handing the request to Ueberauth - redirect(conn, to: o_auth_path(conn, :request, provider, params)) - end - - def request(%Plug.Conn{} = conn, params) do - message = - if params["provider"] do - dgettext("errors", "Unsupported OAuth provider: %{provider}.", - provider: params["provider"] - ) - else - dgettext("errors", "Bad OAuth request.") - end - - conn - |> put_flash(:error, message) - |> redirect(to: "/") - end - - 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, - dgettext("errors", "Failed to authenticate: %{message}.", message: message) - ) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - - def callback(%Plug.Conn{} = conn, params) do - params = callback_params(params) - - with {:ok, registration} <- Authenticator.get_registration(conn) do - auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - - case Repo.get_assoc(registration, :user) do - {:ok, user} -> - create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - - _ -> - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) - - conn - |> 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, dgettext("errors", "Failed to set up user account.")) - |> redirect(external: redirect_uri(conn, params["redirect_uri"])) - end - end - - defp callback_params(%{"state" => state} = params) do - Map.merge(params, Jason.decode!(state)) - end - - 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"], - state: auth_attrs["state"], - scopes: Scopes.fetch_scopes(auth_attrs, []), - nickname: auth_attrs["nickname"], - email: auth_attrs["email"] - }) - end - - 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, _user}} <- - {:create_authorization, do_create_authorization(conn, params)}, - %User{} = user <- Repo.preload(auth, :user).user, - {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do - conn - |> put_session_registration_id(nil) - |> after_create_authorization(auth, params) - else - {:create_authorization, error} -> - {:register, handle_create_authorization_error(conn, error, params)} - - _ -> - {:register, :generic_error} - end - end - - 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 - conn - |> put_session_registration_id(nil) - |> create_authorization( - params, - user: user - ) - else - {:error, changeset} -> - message = - Enum.map(changeset.errors, fn {field, {error, _}} -> - "#{field} #{error}" - end) - |> Enum.join("; ") - - message = - String.replace( - message, - "ap_id has already been taken", - "nickname has already been taken" - ) - - conn - |> put_status(:forbidden) - |> put_flash(:error, "Error: #{message}.") - |> registration_details(params) - - _ -> - {:register, :generic_error} - end - end - - defp do_create_authorization(conn, auth_attrs, user \\ nil) - - defp do_create_authorization( - %Plug.Conn{} = conn, - %{ - "authorization" => - %{ - "client_id" => client_id, - "redirect_uri" => redirect_uri - } = auth_attrs - }, - user - ) do - with {_, {:ok, %User{} = user}} <- - {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, - %App{} = app <- Repo.get_by(App, client_id: client_id), - true <- redirect_uri in String.split(app.redirect_uris), - requested_scopes <- Scopes.fetch_scopes(auth_attrs, app.scopes), - {:ok, auth} <- do_create_authorization(user, app, requested_scopes) do - {:ok, auth, user} - end - end - - defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes) - when is_list(requested_scopes) do - with {:account_status, :active} <- {:account_status, User.account_status(user)}, - {:ok, scopes} <- validate_scopes(app, requested_scopes), - {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do - {:ok, auth} - end - end - - # Note: intended to be a private function but opened for AccountController that logs in on signup - @doc "If checks pass, creates authorization and token for given user, app and requested scopes." - def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested_scopes) do - with {:ok, auth} <- do_create_authorization(user, app, requested_scopes), - {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, - {:ok, token} <- Token.exchange_token(app, auth) do - {:ok, token} - end - end - - # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) - - defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - - defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - - defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), - do: put_session(conn, :registration_id, registration_id) - - defp build_and_response_mfa_token(user, auth) do - with {:ok, token} <- MFA.Token.create(user, auth) do - MFAView.render("mfa_response.json", %{token: token, user: user}) - end - end - - @spec validate_scopes(App.t(), map() | list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(%App{} = app, params) when is_map(params) do - requested_scopes = Scopes.fetch_scopes(params, app.scopes) - validate_scopes(app, requested_scopes) - end - - defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do - Scopes.validate(requested_scopes, 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/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex deleted file mode 100644 index f55247ebd..000000000 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.OAuthView do - use Pleroma.Web, :view - import Phoenix.HTML.Form - - alias Pleroma.Web.OAuth.Token.Utils - - def render("token.json", %{token: token} = opts) do - response = %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: expires_in(), - scope: Enum.join(token.scopes, " "), - created_at: Utils.format_created_at(token) - } - - if user = opts[:user] do - response - |> Map.put(:me, user.ap_id) - else - response - end - end - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex deleted file mode 100644 index 6f06f1431..000000000 --- a/lib/pleroma/web/oauth/scopes.ex +++ /dev/null @@ -1,76 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Scopes do - @moduledoc """ - Functions for dealing with scopes. - """ - - alias Pleroma.Plugs.OAuthScopesPlug - - @doc """ - Fetch scopes from request params. - - Note: `scopes` is used by Mastodon — supporting it but sticking to - OAuth's standard `scope` wherever we control it - """ - @spec fetch_scopes(map() | struct(), list()) :: list() - - def fetch_scopes(params, default) do - parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default) - end - - def parse_scopes(scopes, _default) when is_list(scopes) do - Enum.filter(scopes, &(&1 not in [nil, ""])) - end - - def parse_scopes(scopes, default) when is_binary(scopes) do - scopes - |> to_list - |> parse_scopes(default) - end - - def parse_scopes(_, default) do - default - end - - @doc """ - Convert scopes string to list - """ - @spec to_list(binary()) :: [binary()] - def to_list(nil), do: [] - - def to_list(str) do - str - |> String.trim() - |> String.split(~r/[\s,]+/) - end - - @doc """ - Convert scopes list to string - """ - @spec to_string(list()) :: binary() - def to_string(scopes), do: Enum.join(scopes, " ") - - @doc """ - Validates scopes. - """ - @spec validate(list() | nil, list()) :: - {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], - do: {:error, :missing_scopes} - - def validate(scopes, app_scopes) do - case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do - ^scopes -> {:ok, scopes} - _ -> {:error, :unsupported_scopes} - end - end - - def contains_admin_scopes?(scopes) do - scopes - |> OAuthScopesPlug.filter_descendants(["admin"]) - |> Enum.any?() - end -end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex deleted file mode 100644 index de37998f2..000000000 --- a/lib/pleroma/web/oauth/token.ex +++ /dev/null @@ -1,135 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token do - use Ecto.Schema - - import Ecto.Changeset - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Query - - @type t :: %__MODULE__{} - - schema "oauth_tokens" do - field(:token, :string) - field(:refresh_token, :string) - field(:scopes, {:array, :string}, default: []) - field(:valid_until, :naive_datetime_usec) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - belongs_to(:app, App) - - timestamps() - end - - @doc "Gets token for app by access 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 - Query.get_by_app(app_id) - |> Query.get_by_token(token) - |> Repo.find_resource() - end - - @doc "Gets token for app by refresh token" - @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} - def get_by_refresh_token(%App{id: app_id} = _app, token) do - Query.get_by_app(app_id) - |> Query.get_by_refresh_token(token) - |> Query.preload([:user]) - |> Repo.find_resource() - end - - @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} - def exchange_token(app, auth) do - with {:ok, auth} <- Authorization.use_token(auth), - true <- auth.app_id == app.id do - user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} - - create( - app, - user, - %{scopes: auth.scopes} - ) - end - end - - defp put_token(changeset) do - changeset - |> change(%{token: Token.Utils.generate_token()}) - |> validate_required([:token]) - |> unique_constraint(:token) - end - - defp put_refresh_token(changeset, attrs) do - refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) - - changeset - |> change(%{refresh_token: refresh_token}) - |> validate_required([:refresh_token]) - |> unique_constraint(:refresh_token) - end - - defp put_valid_until(changeset, attrs) do - expires_in = - Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) - - changeset - |> change(%{valid_until: expires_in}) - |> validate_required([:valid_until]) - end - - @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} - def create(%App{} = app, %User{} = user, attrs \\ %{}) do - with {:ok, token} <- do_create(app, user, attrs) do - if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do - Pleroma.Workers.PurgeExpiredToken.enqueue(%{ - token_id: token.id, - valid_until: DateTime.from_naive!(token.valid_until, "Etc/UTC"), - mod: __MODULE__ - }) - end - - {:ok, token} - end - end - - defp do_create(app, user, attrs) do - %__MODULE__{user_id: user.id, app_id: app.id} - |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) - |> validate_required([:scopes, :app_id]) - |> put_valid_until(attrs) - |> put_token() - |> put_refresh_token(attrs) - |> Repo.insert() - end - - def delete_user_tokens(%User{id: user_id}) do - Query.get_by_user(user_id) - |> Repo.delete_all() - end - - def delete_user_token(%User{id: user_id}, token_id) do - Query.get_by_user(user_id) - |> Query.get_by_id(token_id) - |> Repo.delete_all() - end - - def get_user_tokens(%User{id: user_id}) do - Query.get_by_user(user_id) - |> Query.preload([:app]) - |> Repo.all() - end - - def is_expired?(%__MODULE__{valid_until: valid_until}) do - NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 - 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/query.ex b/lib/pleroma/web/oauth/token/query.ex deleted file mode 100644 index fd6d9b112..000000000 --- a/lib/pleroma/web/oauth/token/query.ex +++ /dev/null @@ -1,49 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Query do - @moduledoc """ - Contains queries for OAuth Token. - """ - - import Ecto.Query, only: [from: 2] - - @type query :: Ecto.Queryable.t() | Token.t() - - alias Pleroma.Web.OAuth.Token - - @spec get_by_refresh_token(query, String.t()) :: query - def get_by_refresh_token(query \\ Token, refresh_token) do - from(q in query, where: q.refresh_token == ^refresh_token) - end - - @spec get_by_token(query, String.t()) :: query - def get_by_token(query \\ Token, token) do - from(q in query, where: q.token == ^token) - end - - @spec get_by_app(query, String.t()) :: query - def get_by_app(query \\ Token, app_id) do - from(q in query, where: q.app_id == ^app_id) - end - - @spec get_by_id(query, String.t()) :: query - def get_by_id(query \\ Token, id) do - from(q in query, where: q.id == ^id) - end - - @spec get_by_user(query, String.t()) :: query - def get_by_user(query \\ Token, user_id) do - from(q in query, where: q.user_id == ^user_id) - end - - @spec preload(query, any) :: query - def preload(query \\ Token, assoc_preload \\ []) - - def preload(query, assoc_preload) when is_list(assoc_preload) do - from(q in query, preload: ^assoc_preload) - end - - def preload(query, _assoc_preload), do: query -end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex deleted file mode 100644 index 625b0fde2..000000000 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ /dev/null @@ -1,58 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do - @moduledoc """ - Functions for dealing with refresh token strategy. - """ - - alias Pleroma.Config - alias Pleroma.Repo - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.Revoke - - @doc """ - Will grant access token by refresh token. - """ - @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} - def grant(token) do - access_token = Repo.preload(token, [:user, :app]) - - result = - Repo.transaction(fn -> - token_params = %{ - app: access_token.app, - user: access_token.user, - scopes: access_token.scopes - } - - access_token - |> revoke_access_token() - |> create_access_token(token_params) - end) - - case result do - {:ok, {:error, reason}} -> {:error, reason} - {:ok, {:ok, token}} -> {:ok, token} - {:error, reason} -> {:error, reason} - end - end - - defp revoke_access_token(token) do - Revoke.revoke(token) - end - - defp create_access_token({:error, error}, _), do: {:error, error} - - defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do - Token.create(app, user, add_refresh_token(token_params, token.refresh_token)) - end - - defp add_refresh_token(params, token) do - case Config.get([:oauth2, :issue_new_refresh_token], false) do - true -> Map.put(params, :refresh_token, token) - false -> params - end - end -end diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex deleted file mode 100644 index 069c1ee21..000000000 --- a/lib/pleroma/web/oauth/token/strategy/revoke.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do - @moduledoc """ - Functions for dealing with revocation. - """ - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Token - - @doc "Finds and revokes access token for app and by token" - @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} - def revoke(%App{} = app, %{"token" => token} = _attrs) do - with {:ok, token} <- Token.get_by_token(app, token), - do: revoke(token) - end - - @doc "Revokes access token" - @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} - def revoke(%Token{} = token) do - Repo.delete(token) - end -end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex deleted file mode 100644 index 43aeab6b0..000000000 --- a/lib/pleroma/web/oauth/token/utils.ex +++ /dev/null @@ -1,72 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Utils do - @moduledoc """ - Auxiliary functions for dealing with tokens. - """ - - alias Pleroma.Repo - alias Pleroma.Web.OAuth.App - - @doc "Fetch app by client credentials from request" - @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} - def fetch_app(conn) do - res = - conn - |> fetch_client_credentials() - |> fetch_client - - case res do - %App{} = app -> {:ok, app} - _ -> {:error, :not_found} - end - end - - defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do - Repo.get_by(App, client_id: id, client_secret: secret) - end - - defp fetch_client({_id, _secret}), do: nil - - defp fetch_client_credentials(conn) do - # Per RFC 6749, HTTP Basic is preferred to body params - with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), - {:ok, decoded} <- Base.decode64(encoded), - [id, secret] <- - Enum.map( - String.split(decoded, ":"), - fn s -> URI.decode_www_form(s) end - ) do - {id, secret} - else - _ -> {conn.params["client_id"], conn.params["client_secret"]} - end - end - - @doc "convert token inserted_at to unix timestamp" - def format_created_at(%{inserted_at: inserted_at} = _token) do - inserted_at - |> DateTime.from_naive!("Etc/UTC") - |> DateTime.to_unix() - end - - @doc false - @spec generate_token(keyword()) :: binary() - def generate_token(opts \\ []) do - opts - |> Keyword.get(:size, 32) - |> :crypto.strong_rand_bytes() - |> Base.url_encode64(padding: false) - end - - # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be - # decoding it. Investigate sometime. - def fix_padding(token) do - token - |> URI.decode() - |> Base.url_decode64!(padding: false) - |> Base.url_encode64(padding: false) - end -end -- cgit v1.2.3 From e8e4034c4879ebf0bb7fcc7606c97a3957a0ba06 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 19:55:55 +0300 Subject: metadata providers consistency --- lib/pleroma/web/metadata/feed.ex | 23 ---- lib/pleroma/web/metadata/opengraph.ex | 119 --------------------- lib/pleroma/web/metadata/provider.ex | 7 -- lib/pleroma/web/metadata/providers/feed.ex | 23 ++++ lib/pleroma/web/metadata/providers/opengraph.ex | 119 +++++++++++++++++++++ lib/pleroma/web/metadata/providers/provider.ex | 7 ++ lib/pleroma/web/metadata/providers/rel_me.ex | 19 ++++ .../web/metadata/providers/restrict_indexing.ex | 24 +++++ lib/pleroma/web/metadata/providers/twitter_card.ex | 112 +++++++++++++++++++ lib/pleroma/web/metadata/rel_me.ex | 19 ---- lib/pleroma/web/metadata/restrict_indexing.ex | 24 ----- lib/pleroma/web/metadata/twitter_card.ex | 112 ------------------- 12 files changed, 304 insertions(+), 304 deletions(-) delete mode 100644 lib/pleroma/web/metadata/feed.ex delete mode 100644 lib/pleroma/web/metadata/opengraph.ex delete mode 100644 lib/pleroma/web/metadata/provider.ex create mode 100644 lib/pleroma/web/metadata/providers/feed.ex create mode 100644 lib/pleroma/web/metadata/providers/opengraph.ex create mode 100644 lib/pleroma/web/metadata/providers/provider.ex create mode 100644 lib/pleroma/web/metadata/providers/rel_me.ex create mode 100644 lib/pleroma/web/metadata/providers/restrict_indexing.ex create mode 100644 lib/pleroma/web/metadata/providers/twitter_card.ex delete mode 100644 lib/pleroma/web/metadata/rel_me.ex delete mode 100644 lib/pleroma/web/metadata/restrict_indexing.ex delete mode 100644 lib/pleroma/web/metadata/twitter_card.ex (limited to 'lib') diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex deleted file mode 100644 index bd1459a17..000000000 --- a/lib/pleroma/web/metadata/feed.ex +++ /dev/null @@ -1,23 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.Feed do - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Router.Helpers - - @behaviour Provider - - @impl Provider - def build_tags(%{user: user}) do - [ - {:link, - [ - rel: "alternate", - type: "application/atom+xml", - href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" - ], []} - ] - end -end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex deleted file mode 100644 index bb1b23208..000000000 --- a/lib/pleroma/web/metadata/opengraph.ex +++ /dev/null @@ -1,119 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.OpenGraph do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{ - object: object, - url: url, - user: user - }) do - attachments = build_attachments(object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - ": “" <> scrubbed_content <> "”" - else - "" - end - - # Most previews only show og:title which is inconvenient. Instagram - # hacks this by putting the description in the title and making the - # description longer prefixed by how many likes and shares the post - # has. Here we use the descriptive nickname in the title, and expand - # the full account & nickname in the description. We also use the cute^Wevil - # smart quotes around the status text like Instagram, too. - [ - {:meta, - [ - property: "og:title", - content: "#{user.name}" <> content - ], []}, - {:meta, [property: "og:url", content: url], []}, - {:meta, - [ - property: "og:description", - content: "#{Utils.user_name_string(user)}" <> content - ], []}, - {:meta, [property: "og:type", content: "website"], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - {:meta, - [ - property: "og:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, - {:meta, [property: "og:description", content: truncated_bio], []}, - {:meta, [property: "og:type", content: "website"], []}, - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - ] - end - end - - defp build_attachments(%{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image - # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} - | acc - ] - - "video" -> - [ - {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - end) - end - - defp build_attachments(_), do: [] -end diff --git a/lib/pleroma/web/metadata/provider.ex b/lib/pleroma/web/metadata/provider.ex deleted file mode 100644 index 767288f9c..000000000 --- a/lib/pleroma/web/metadata/provider.ex +++ /dev/null @@ -1,7 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.Provider do - @callback build_tags(map()) :: list() -end diff --git a/lib/pleroma/web/metadata/providers/feed.ex b/lib/pleroma/web/metadata/providers/feed.ex new file mode 100644 index 000000000..bd1459a17 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/feed.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Feed do + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Router.Helpers + + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + [ + {:link, + [ + rel: "alternate", + type: "application/atom+xml", + href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom" + ], []} + ] + end +end diff --git a/lib/pleroma/web/metadata/providers/opengraph.ex b/lib/pleroma/web/metadata/providers/opengraph.ex new file mode 100644 index 000000000..bb1b23208 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/opengraph.ex @@ -0,0 +1,119 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraph do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{ + object: object, + url: url, + user: user + }) do + attachments = build_attachments(object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + ": “" <> scrubbed_content <> "”" + else + "" + end + + # Most previews only show og:title which is inconvenient. Instagram + # hacks this by putting the description in the title and making the + # description longer prefixed by how many likes and shares the post + # has. Here we use the descriptive nickname in the title, and expand + # the full account & nickname in the description. We also use the cute^Wevil + # smart quotes around the status text like Instagram, too. + [ + {:meta, + [ + property: "og:title", + content: "#{user.name}" <> content + ], []}, + {:meta, [property: "og:url", content: url], []}, + {:meta, + [ + property: "og:description", + content: "#{Utils.user_name_string(user)}" <> content + ], []}, + {:meta, [property: "og:type", content: "website"], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], + []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + {:meta, + [ + property: "og:title", + content: Utils.user_name_string(user) + ], []}, + {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, + {:meta, [property: "og:description", content: truncated_bio], []}, + {:meta, [property: "og:type", content: "website"], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + ] + end + end + + defp build_attachments(%{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image + # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image:width", content: 150], []}, + {:meta, [property: "og:image:height", content: 150], []} + | acc + ] + + "video" -> + [ + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + end) + end + + defp build_attachments(_), do: [] +end diff --git a/lib/pleroma/web/metadata/providers/provider.ex b/lib/pleroma/web/metadata/providers/provider.ex new file mode 100644 index 000000000..767288f9c --- /dev/null +++ b/lib/pleroma/web/metadata/providers/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.Provider do + @callback build_tags(map()) :: list() +end diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex new file mode 100644 index 000000000..8905c9c72 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RelMe do + alias Pleroma.Web.Metadata.Providers.Provider + @behaviour Provider + + @impl Provider + def build_tags(%{user: user}) do + bio_tree = Floki.parse_fragment!(user.bio) + + (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ + Floki.attribute(bio_tree, "a[rel~=me]", "href")) + |> Enum.map(fn link -> + {:link, [rel: "me", href: link], []} + end) + end +end diff --git a/lib/pleroma/web/metadata/providers/restrict_indexing.ex b/lib/pleroma/web/metadata/providers/restrict_indexing.ex new file mode 100644 index 000000000..a1dcb6e15 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/restrict_indexing.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do + @behaviour Pleroma.Web.Metadata.Providers.Provider + + @moduledoc """ + Restricts indexing of remote users. + """ + + @impl true + def build_tags(%{user: %{local: true, discoverable: true}}), do: [] + + def build_tags(_) do + [ + {:meta, + [ + name: "robots", + content: "noindex, noarchive" + ], []} + ] + end +end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex new file mode 100644 index 000000000..df34b033f --- /dev/null +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -0,0 +1,112 @@ +# Pleroma: A lightweight social networking server + +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.TwitterCard do + alias Pleroma.User + alias Pleroma.Web.Metadata + alias Pleroma.Web.Metadata.Providers.Provider + alias Pleroma.Web.Metadata.Utils + + @behaviour Provider + @media_types ["image", "audio", "video"] + + @impl Provider + def build_tags(%{activity_id: id, object: object, user: user}) do + attachments = build_attachments(id, object) + scrubbed_content = Utils.scrub_html_and_truncate(object) + # Zero width space + content = + if scrubbed_content != "" and scrubbed_content != "\u200B" do + "“" <> scrubbed_content <> "”" + else + "" + end + + [ + title_tag(user), + {:meta, [property: "twitter:description", content: content], []} + ] ++ + if attachments == [] or Metadata.activity_nsfw?(object) do + [ + image_tag(user), + {:meta, [property: "twitter:card", content: "summary"], []} + ] + else + attachments + end + end + + @impl Provider + def build_tags(%{user: user}) do + with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do + [ + title_tag(user), + {:meta, [property: "twitter:description", content: truncated_bio], []}, + image_tag(user), + {:meta, [property: "twitter:card", content: "summary"], []} + ] + end + end + + defp title_tag(user) do + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + end + + def image_tag(user) do + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + end + + defp build_attachments(id, %{data: %{"attachment" => attachments}}) do + Enum.reduce(attachments, [], fn attachment, acc -> + rendered_tags = + Enum.reduce(attachment["url"], [], fn url, acc -> + # TODO: Add additional properties to objects when we have the data available. + case Utils.fetch_media_type(@media_types, url["mediaType"]) do + "audio" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "80"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []} + | acc + ] + + "image" -> + [ + {:meta, [property: "twitter:card", content: "summary_large_image"], []}, + {:meta, + [ + property: "twitter:player", + content: Utils.attachment_url(url["href"]) + ], []} + | acc + ] + + # TODO: Need the true width and height values here or Twitter renders an iFrame with + # a bad aspect ratio + "video" -> + [ + {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [property: "twitter:player", content: player_url(id)], []}, + {:meta, [property: "twitter:player:width", content: "480"], []}, + {:meta, [property: "twitter:player:height", content: "480"], []} + | acc + ] + + _ -> + acc + end + end) + + acc ++ rendered_tags + 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 +end diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex deleted file mode 100644 index 8905c9c72..000000000 --- a/lib/pleroma/web/metadata/rel_me.ex +++ /dev/null @@ -1,19 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RelMe do - alias Pleroma.Web.Metadata.Providers.Provider - @behaviour Provider - - @impl Provider - def build_tags(%{user: user}) do - bio_tree = Floki.parse_fragment!(user.bio) - - (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ - Floki.attribute(bio_tree, "a[rel~=me]", "href")) - |> Enum.map(fn link -> - {:link, [rel: "me", href: link], []} - end) - end -end diff --git a/lib/pleroma/web/metadata/restrict_indexing.ex b/lib/pleroma/web/metadata/restrict_indexing.ex deleted file mode 100644 index a1dcb6e15..000000000 --- a/lib/pleroma/web/metadata/restrict_indexing.ex +++ /dev/null @@ -1,24 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.RestrictIndexing do - @behaviour Pleroma.Web.Metadata.Providers.Provider - - @moduledoc """ - Restricts indexing of remote users. - """ - - @impl true - def build_tags(%{user: %{local: true, discoverable: true}}), do: [] - - def build_tags(_) do - [ - {:meta, - [ - name: "robots", - content: "noindex, noarchive" - ], []} - ] - end -end diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex deleted file mode 100644 index df34b033f..000000000 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ /dev/null @@ -1,112 +0,0 @@ -# Pleroma: A lightweight social networking server - -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Metadata.Providers.TwitterCard do - alias Pleroma.User - alias Pleroma.Web.Metadata - alias Pleroma.Web.Metadata.Providers.Provider - alias Pleroma.Web.Metadata.Utils - - @behaviour Provider - @media_types ["image", "audio", "video"] - - @impl Provider - def build_tags(%{activity_id: id, object: object, user: user}) do - attachments = build_attachments(id, object) - scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - "“" <> scrubbed_content <> "”" - else - "" - end - - [ - title_tag(user), - {:meta, [property: "twitter:description", content: content], []} - ] ++ - if attachments == [] or Metadata.activity_nsfw?(object) do - [ - image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} - ] - else - attachments - end - end - - @impl Provider - def build_tags(%{user: user}) do - with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do - [ - title_tag(user), - {:meta, [property: "twitter:description", content: truncated_bio], []}, - image_tag(user), - {:meta, [property: "twitter:card", content: "summary"], []} - ] - end - end - - defp title_tag(user) do - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} - end - - def image_tag(user) do - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} - end - - defp build_attachments(id, %{data: %{"attachment" => attachments}}) do - Enum.reduce(attachments, [], fn attachment, acc -> - rendered_tags = - Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - case Utils.fetch_media_type(@media_types, url["mediaType"]) do - "audio" -> - [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "80"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []} - | acc - ] - - "image" -> - [ - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, - {:meta, - [ - property: "twitter:player", - content: Utils.attachment_url(url["href"]) - ], []} - | acc - ] - - # TODO: Need the true width and height values here or Twitter renders an iFrame with - # a bad aspect ratio - "video" -> - [ - {:meta, [property: "twitter:card", content: "player"], []}, - {:meta, [property: "twitter:player", content: player_url(id)], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "480"], []} - | acc - ] - - _ -> - acc - end - end) - - acc ++ rendered_tags - 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 -end -- cgit v1.2.3 From fc7151a9c4cbd2fb122d717f54de4b30acffea36 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 23 Jun 2020 20:02:53 +0300 Subject: more files renamings --- lib/phoenix/transports/web_socket/raw.ex | 89 ++++++ lib/pleroma/web.ex | 234 +++++++++++++++ .../web/mongoose_im/mongoose_im_controller.ex | 46 +++ .../web/mongooseim/mongoose_im_controller.ex | 46 --- lib/pleroma/web/o_status/o_status_controller.ex | 151 ++++++++++ lib/pleroma/web/ostatus/ostatus_controller.ex | 151 ---------- lib/pleroma/web/plug.ex | 8 + lib/pleroma/web/push.ex | 37 +++ lib/pleroma/web/push/push.ex | 37 --- lib/pleroma/web/rich_media/parsers/o_embed.ex | 29 ++ .../web/rich_media/parsers/oembed_parser.ex | 29 -- lib/pleroma/web/streamer.ex | 331 +++++++++++++++++++++ lib/pleroma/web/streamer/streamer.ex | 331 --------------------- lib/pleroma/web/twitter_api/controller.ex | 100 +++++++ .../web/twitter_api/twitter_api_controller.ex | 100 ------- lib/pleroma/web/web.ex | 239 --------------- lib/pleroma/web/web_finger.ex | 201 +++++++++++++ lib/pleroma/web/web_finger/web_finger.ex | 201 ------------- lib/pleroma/web/xml.ex | 45 +++ lib/pleroma/web/xml/xml.ex | 45 --- lib/pleroma/xml_builder.ex | 49 +++ lib/transports.ex | 89 ------ lib/xml_builder.ex | 49 --- 23 files changed, 1320 insertions(+), 1317 deletions(-) create mode 100644 lib/phoenix/transports/web_socket/raw.ex create mode 100644 lib/pleroma/web.ex create mode 100644 lib/pleroma/web/mongoose_im/mongoose_im_controller.ex delete mode 100644 lib/pleroma/web/mongooseim/mongoose_im_controller.ex create mode 100644 lib/pleroma/web/o_status/o_status_controller.ex delete mode 100644 lib/pleroma/web/ostatus/ostatus_controller.ex create mode 100644 lib/pleroma/web/plug.ex create mode 100644 lib/pleroma/web/push.ex delete mode 100644 lib/pleroma/web/push/push.ex create mode 100644 lib/pleroma/web/rich_media/parsers/o_embed.ex delete mode 100644 lib/pleroma/web/rich_media/parsers/oembed_parser.ex create mode 100644 lib/pleroma/web/streamer.ex delete mode 100644 lib/pleroma/web/streamer/streamer.ex create mode 100644 lib/pleroma/web/twitter_api/controller.ex delete mode 100644 lib/pleroma/web/twitter_api/twitter_api_controller.ex delete mode 100644 lib/pleroma/web/web.ex create mode 100644 lib/pleroma/web/web_finger.ex delete mode 100644 lib/pleroma/web/web_finger/web_finger.ex create mode 100644 lib/pleroma/web/xml.ex delete mode 100644 lib/pleroma/web/xml/xml.ex create mode 100644 lib/pleroma/xml_builder.ex delete mode 100644 lib/transports.ex delete mode 100644 lib/xml_builder.ex (limited to 'lib') diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex new file mode 100644 index 000000000..aab7fad99 --- /dev/null +++ b/lib/phoenix/transports/web_socket/raw.ex @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Phoenix.Transports.WebSocket.Raw do + import Plug.Conn, + only: [ + fetch_query_params: 1, + send_resp: 3 + ] + + alias Phoenix.Socket.Transport + + def default_config do + [ + timeout: 60_000, + transport_log: false, + cowboy: Phoenix.Endpoint.CowboyWebSocket + ] + end + + def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do + {_, opts} = handler.__transport__(transport) + + conn = + conn + |> fetch_query_params + |> Transport.transport_log(opts[:transport_log]) + |> Transport.force_ssl(handler, endpoint, opts) + |> Transport.check_origin(handler, endpoint, opts) + + case conn do + %{halted: false} = conn -> + case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do + {:ok, socket} -> + {:ok, conn, {__MODULE__, {socket, opts}}} + + :error -> + send_resp(conn, :forbidden, "") + {:error, conn} + end + + _ -> + {:error, conn} + end + end + + def init(conn, _) do + send_resp(conn, :bad_request, "") + {:error, conn} + end + + def ws_init({socket, config}) do + Process.flag(:trap_exit, true) + {:ok, %{socket: socket}, config[:timeout]} + end + + def ws_handle(op, data, state) do + state.socket.handler + |> apply(:handle, [op, data, state]) + |> case do + {op, data} -> + {:reply, {op, data}, state} + + {op, data, state} -> + {:reply, {op, data}, state} + + %{} = state -> + {:ok, state} + + _ -> + {:ok, state} + end + end + + def ws_info({_, _} = tuple, state) do + {:reply, tuple, state} + end + + def ws_info(_tuple, state), do: {:ok, state} + + def ws_close(state) do + ws_handle(:closed, :normal, state) + end + + def ws_terminate(reason, state) do + ws_handle(:closed, reason, state) + end +end diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex new file mode 100644 index 000000000..9ca52733d --- /dev/null +++ b/lib/pleroma/web.ex @@ -0,0 +1,234 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web do + @moduledoc """ + A module that keeps using definitions for controllers, + views and so on. + + This can be used in your application as: + + use Pleroma.Web, :controller + use Pleroma.Web, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. + """ + + alias Pleroma.Plugs.EnsureAuthenticatedPlug + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + + 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) + + defp set_put_layout(conn, _) do + put_layout(conn, Pleroma.Config.get(:app_layout, "app.html")) + end + + # Marks plugs intentionally skipped and blocks their execution if present in plugs chain + defp skip_plug(conn, plug_modules) do + plug_modules + |> List.wrap() + |> Enum.reduce( + conn, + fn plug_module, conn -> + try do + plug_module.skip_plug(conn) + rescue + UndefinedFunctionError -> + raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code." + end + end + ) + end + + # Executed just before actual controller action, invokes before-action hooks (callbacks) + defp action(conn, params) do + with %{halted: false} = conn <- maybe_drop_authentication_if_oauth_check_ignored(conn), + %{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn), + %{halted: false} = conn <- maybe_perform_authenticated_check(conn), + %{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do + super(conn, params) + end + end + + # For non-authenticated API actions, drops auth info if OAuth scopes check was ignored + # (neither performed nor explicitly skipped) + defp maybe_drop_authentication_if_oauth_check_ignored(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + OAuthScopesPlug.drop_auth_info(conn) + else + conn + end + end + + # Ensures instance is public -or- user is authenticated if such check was scheduled + defp maybe_perform_public_or_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do + EnsurePublicOrAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Ensures user is authenticated if such check was scheduled + # Note: runs prior to action even if it was already executed earlier in plug chain + # (since OAuthScopesPlug has option of proceeding unauthenticated) + defp maybe_perform_authenticated_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do + EnsureAuthenticatedPlug.call(conn, %{}) + else + conn + end + end + + # Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check + defp maybe_halt_on_missing_oauth_scopes_check(conn) do + if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and + not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do + conn + |> render_error( + :forbidden, + "Security violation: OAuth scopes check was neither handled nor explicitly skipped." + ) + |> halt() + else + conn + end + end + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/pleroma/web/templates", + namespace: Pleroma.Web + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] + + import Pleroma.Web.ErrorHelpers + import Pleroma.Web.Gettext + import Pleroma.Web.Router.Helpers + + require Logger + + @doc "Same as `render/3` but wrapped in a rescue block" + def safe_render(view, template, assigns \\ %{}) do + Phoenix.View.render(view, template, assigns) + rescue + error -> + Logger.error( + "#{__MODULE__} failed to render #{inspect({view, template})}\n" <> + Exception.format(:error, error, __STACKTRACE__) + ) + + nil + end + + @doc """ + Same as `render_many/4` but wrapped in rescue block. + """ + def safe_render_many(collection, view, template, assigns \\ %{}) do + Enum.map(collection, fn resource -> + as = Map.get(assigns, :as) || view.__resource__ + assigns = Map.put(assigns, as, resource) + safe_render(view, template, assigns) + end) + |> Enum.filter(& &1) + end + end + end + + def router do + quote do + use Phoenix.Router + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Phoenix.Channel + import Pleroma.Web.Gettext + end + end + + def plug do + quote do + @behaviour Pleroma.Web.Plug + @behaviour Plug + + @doc """ + Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain. + """ + def skip_plug(conn) do + PlugHelper.append_to_private_list( + conn, + PlugHelper.skipped_plugs_list_id(), + __MODULE__ + ) + end + + @impl Plug + @doc """ + Before-plug hook that + * ensures the plug is not skipped + * processes `:if_func` / `:unless_func` functional pre-run conditions + * adds plug to the list of called plugs and calls `perform/2` if checks are passed + + Note: multiple invocations of the same plug (with different or same options) are allowed. + """ + def call(%Plug.Conn{} = conn, options) do + if PlugHelper.plug_skipped?(conn, __MODULE__) || + (options[:if_func] && !options[:if_func].(conn)) || + (options[:unless_func] && options[:unless_func].(conn)) do + conn + else + conn = + PlugHelper.append_to_private_list( + conn, + PlugHelper.called_plugs_list_id(), + __MODULE__ + ) + + apply(__MODULE__, :perform, [conn, options]) + end + end + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end + + def base_url do + Pleroma.Web.Endpoint.url() + end +end diff --git a/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex new file mode 100644 index 000000000..6cbbe8fd8 --- /dev/null +++ b/lib/pleroma/web/mongoose_im/mongoose_im_controller.ex @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MongooseIM.MongooseIMController do + use Pleroma.Web, :controller + + alias Pleroma.Plugs.AuthenticationPlug + alias Pleroma.Plugs.RateLimiter + alias Pleroma.Repo + alias Pleroma.User + + 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, deactivated: false) do + conn + |> json(true) + else + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end + + def check_password(conn, %{"user" => username, "pass" => password}) do + with %User{password_hash: password_hash, deactivated: false} <- + Repo.get_by(User, nickname: username, local: true), + true <- AuthenticationPlug.checkpw(password, password_hash) do + conn + |> json(true) + else + false -> + conn + |> put_status(:forbidden) + |> json(false) + + _ -> + conn + |> put_status(:not_found) + |> json(false) + end + end +end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex deleted file mode 100644 index 6cbbe8fd8..000000000 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ /dev/null @@ -1,46 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MongooseIM.MongooseIMController do - use Pleroma.Web, :controller - - alias Pleroma.Plugs.AuthenticationPlug - alias Pleroma.Plugs.RateLimiter - alias Pleroma.Repo - alias Pleroma.User - - 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, deactivated: false) do - conn - |> json(true) - else - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end - - def check_password(conn, %{"user" => username, "pass" => password}) do - with %User{password_hash: password_hash, deactivated: false} <- - Repo.get_by(User, nickname: username, local: true), - true <- AuthenticationPlug.checkpw(password, password_hash) do - conn - |> json(true) - else - false -> - conn - |> put_status(:forbidden) - |> json(false) - - _ -> - conn - |> put_status(:not_found) - |> json(false) - end - end -end diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex new file mode 100644 index 000000000..de1b0b3f0 --- /dev/null +++ b/lib/pleroma/web/o_status/o_status_controller.ex @@ -0,0 +1,151 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OStatus.OStatusController do + use Pleroma.Web, :controller + + 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.Visibility + alias Pleroma.Web.Endpoint + alias Pleroma.Web.Metadata.PlayerView + alias Pleroma.Web.Router + + plug(Pleroma.Plugs.EnsureAuthenticatedPlug, + unless_func: &Pleroma.Web.FederatingPlug.federating?/1 + ) + + plug( + RateLimiter, + [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] + ) + + plug( + Pleroma.Plugs.SetFormatPlug + when action in [:object, :activity, :notice] + ) + + action_fallback(:errors) + + def object(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :object) + end + + def object(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- + {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def activity(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :activity) + end + + def activity(%{assigns: %{format: format}} = conn, _params) do + with id <- Endpoint.url() <> conn.request_path, + {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)} do + case format do + _ -> redirect(conn, to: "/notice/#{activity.id}") + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e + end + end + + def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do + with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)}, + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do + cond do + format in ["json", "activity+json"] -> + if activity.local do + %{data: %{"id" => redirect_url}} = Object.normalize(activity) + redirect(conn, external: redirect_url) + else + {:error, :not_found} + end + + activity.data["type"] == "Create" -> + %Object{} = object = Object.normalize(activity) + + RedirectController.redirector_with_meta( + conn, + %{ + activity_id: activity.id, + object: object, + url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), + user: user + } + ) + + true -> + RedirectController.redirector(conn, nil) + end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + conn + |> put_status(404) + |> RedirectController.redirector(nil, 404) + + e -> + e + end + end + + # Returns an HTML embedded