diff --git a/CHANGELOG.md b/CHANGELOG.md index e2249f897..22f199b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Admin API: Return link alongside with token on password reset - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. +- **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes` - Admin API: Return `total` when querying for reports - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Admin API: Return link alongside with token on password reset @@ -53,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - User notification settings: Add `privacy_option` option. - Support for custom Elixir modules (such as MRF policies) - User settings: Add _This account is a_ option. +- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
API Changes @@ -84,6 +86,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ActivityPub: Configurable `type` field of the actors. - Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field. - Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field. +- Captcha: Support native provider +- Captcha: Enable by default
### Fixed @@ -92,6 +96,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - MRF: `Delete` activities being exempt from MRF policies - OTP releases: Not being able to configure OAuth expired token cleanup interval - OTP releases: Not being able to configure HTML sanitization policy +- Favorites timeline now ordered by favorite date instead of post date
API Changes diff --git a/config/config.exs b/config/config.exs index 82d62004c..a9bc1b0aa 100644 --- a/config/config.exs +++ b/config/config.exs @@ -66,9 +66,9 @@ jobs: scheduled_jobs config :pleroma, Pleroma.Captcha, - enabled: false, + enabled: true, seconds_valid: 60, - method: Pleroma.Captcha.Kocaptcha + method: Pleroma.Captcha.Native config :pleroma, :hackney_pools, federation: [ @@ -84,8 +84,6 @@ timeout: 300_000 ] -config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch" - # Upload configuration config :pleroma, Pleroma.Upload, uploader: Pleroma.Uploaders.Local, @@ -563,7 +561,10 @@ base_path: "/oauth", providers: ueberauth_providers -config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies +config :pleroma, + :auth, + enforce_oauth_admin_scope_usage: false, + oauth_consumer_strategies: oauth_consumer_strategies config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false diff --git a/config/description.exs b/config/description.exs index 70e963399..45e4b43f1 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2094,6 +2094,15 @@ type: :group, description: "Authentication / authorization settings", children: [ + %{ + key: :enforce_oauth_admin_scope_usage, + type: :boolean, + description: + "OAuth admin scope requirement toggle. " <> + "If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <> + "(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <> + "`is_admin` user flag grants access to admin-specific actions." + }, %{ key: :auth_template, type: :string, diff --git a/config/test.exs b/config/test.exs index 8b9bf5c77..6f6ab733e 100644 --- a/config/test.exs +++ b/config/test.exs @@ -68,7 +68,9 @@ queues: false, prune: :disabled -config :pleroma, Pleroma.Scheduler, jobs: [] +config :pleroma, Pleroma.Scheduler, + jobs: [], + global: false config :pleroma, Pleroma.ScheduledActivity, daily_user_limit: 2, @@ -95,6 +97,8 @@ config :pleroma, :modules, runtime_dir: "test/fixtures/modules" +config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch" + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index 2cac317de..d98a78af0 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -2,6 +2,13 @@ Authentication is required and the user must be an admin. +Configuration options: + +* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle. + If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes). + If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions. + Note that client app needs to explicitly support admin scopes and request them when obtaining auth token. + ## `GET /api/pleroma/admin/users` ### List users @@ -607,78 +614,29 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - On success: `204`, empty response -## `POST /api/pleroma/admin/reports/:id/respond` +## `POST /api/pleroma/admin/reports/:id/notes` -### Respond to a report +### Create report note - Params: - - `id` - - `status`: required, the message + - `id`: required, report id + - `content`: required, the message - Response: - On failure: - 400 Bad Request `"Invalid parameters"` when `status` is missing - - 403 Forbidden `{"error": "error_msg"}` - - 404 Not Found `"Not found"` - - On success: JSON, created Mastodon Status entity + - On success: `204`, empty response -```json -{ - "account": { ... }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "Your claim is going to be closed", - "created_at": "2019-05-11T17:13:03.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "9ihuiSL1405I65TmEq", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "user", - "id": "9i6dAJqSGSKMzLG2Lo", - "url": "https://pleroma.example.org/users/user", - "username": "user" - }, - { - "acct": "admin", - "id": "9hEkA5JsvAdlSrocam", - "url": "https://pleroma.example.org/users/admin", - "username": "admin" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "Your claim is going to be closed" - }, - "conversation_id": 35, - "in_reply_to_account_acct": null, - "local": true, - "spoiler_text": { - "text/plain": "" - } - }, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb", - "url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq", - "visibility": "direct" -} -``` +## `POST /api/pleroma/admin/reports/:report_id/notes/:id` + +### Delete report note + +- Params: + - `report_id`: required, report id + - `id`: required, note id +- Response: + - On failure: + - 400 Bad Request `"Invalid parameters"` when `status` is missing + - On success: `204`, empty response ## `PUT /api/pleroma/admin/statuses/:id` diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index b3a13833c..a214b6e2f 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -379,13 +379,19 @@ For each pool, the options are: ## Captcha ### Pleroma.Captcha + * `enabled`: Whether the captcha should be shown on registration. * `method`: The method/service to use for captcha. * `seconds_valid`: The time in seconds for which the captcha is valid. ### Captcha providers +#### Pleroma.Captcha.Native + +A built-in captcha provider. Enabled by default. + #### Pleroma.Captcha.Kocaptcha + Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint `https://captcha.kotobank.ch` is hosted by the developer. diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 0adb78fe3..85c9e4954 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do alias Ecto.Changeset alias Pleroma.User alias Pleroma.UserInviteToken - alias Pleroma.Web.OAuth @shortdoc "Manages Pleroma users" @moduledoc File.read!("docs/administration/CLI_tasks/user.md") @@ -354,8 +353,7 @@ def run(["sign_out", nickname]) do start_pleroma() with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do - OAuth.Token.delete_user_tokens(user) - OAuth.Authorization.delete_user_authorizations(user) + User.global_sign_out(user) shell_info("#{nickname} signed out from all apps.") else @@ -393,10 +391,7 @@ defp set_moderator(user, value) do end defp set_admin(user, value) do - {:ok, user} = - user - |> Changeset.change(%{is_admin: value}) - |> User.update_and_set_cache() + {:ok, user} = User.admin_api_update(user, %{is_admin: value}) shell_info("Admin status of #{user.nickname}: #{user.is_admin}") user diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 480b261cf..510d3273c 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Activity do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.ReportNote alias Pleroma.ThreadMute alias Pleroma.User @@ -48,6 +49,8 @@ defmodule Pleroma.Activity do has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id) # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark has_one(:bookmark, Bookmark) + # This is a fake relation, do not use outside of with_preloaded_report_notes + has_many(:report_notes, ReportNote) has_many(:notifications, Notification, on_delete: :delete_all) # Attention: this is a fake relation, don't try to preload it blindly and expect it to work! @@ -114,6 +117,16 @@ def with_preloaded_bookmark(query, %User{} = user) do def with_preloaded_bookmark(query, _), do: query + def with_preloaded_report_notes(query) do + from([a] in query, + left_join: r in ReportNote, + on: a.id == r.activity_id, + preload: [report_notes: r] + ) + end + + def with_preloaded_report_notes(query, _), do: query + def with_set_thread_muted_field(query, %User{} = user) do from([a] in query, left_join: tm in ThreadMute, diff --git a/lib/pleroma/captcha/native.ex b/lib/pleroma/captcha/native.ex new file mode 100644 index 000000000..5306fe1aa --- /dev/null +++ b/lib/pleroma/captcha/native.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha.Native do + import Pleroma.Web.Gettext + alias Pleroma.Captcha.Service + @behaviour Service + + @impl Service + def new do + case Captcha.get() do + {:timeout} -> + %{error: dgettext("errors", "Captcha timeout")} + + {:ok, answer_data, img_binary} -> + %{ + type: :native, + token: token(), + url: "data:image/png;base64," <> Base.encode64(img_binary), + answer_data: answer_data + } + end + end + + @impl Service + def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok + def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")} + + defp token do + 10 + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end +end diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index fcc039710..bad6d505c 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -65,4 +65,16 @@ def delete(key) do def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] + + def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage]) + + def oauth_admin_scopes(scopes) when is_list(scopes) do + Enum.flat_map( + scopes, + fn scope -> + ["admin:#{scope}"] ++ + if enforce_oauth_admin_scope_usage?(), do: [], else: [scope] + end + ) + end end diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 706f089dc..c81477f48 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -128,17 +128,35 @@ def insert_log(%{ {:ok, ModerationLog} | {:error, any} def insert_log(%{ actor: %User{} = actor, - action: "report_response", + action: "report_note", subject: %Activity{} = subject, text: text }) do %ModerationLog{ data: %{ "actor" => user_to_map(actor), - "action" => "report_response", + "action" => "report_note", "subject" => report_to_map(subject), - "text" => text, - "message" => "" + "text" => text + } + } + |> insert_log_entry_with_message() + end + + @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{ + actor: %User{} = actor, + action: "report_note_delete", + subject: %Activity{} = subject, + text: text + }) do + %ModerationLog{ + data: %{ + "actor" => user_to_map(actor), + "action" => "report_note_delete", + "subject" => report_to_map(subject), + "text" => text } } |> insert_log_entry_with_message() @@ -556,12 +574,24 @@ def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, - "action" => "report_response", + "action" => "report_note", "subject" => %{"id" => subject_id, "type" => "report"}, "text" => text } }) do - "@#{actor_nickname} responded with '#{text}' to report ##{subject_id}" + "@#{actor_nickname} added note '#{text}' to report ##{subject_id}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_note_delete", + "subject" => %{"id" => subject_id, "type" => "report"}, + "text" => text + } + }) do + "@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}" end @spec get_log_entry_message(ModerationLog) :: String.t() diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index ff0e59241..eb37b95a6 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -23,6 +23,23 @@ defmodule Pleroma.Object do timestamps() end + def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do + object_position = Map.get(query.aliases, :object, 0) + + join(query, join_type, [{object, object_position}], a in Activity, + on: + fragment( + "COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ", + a.data, + a.data, + object.data, + a.data, + ^activity_type + ), + as: :object_activity + ) + end + def create(data) do Object.change(%Object{}, %{data: data}) |> Repo.insert() diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index 9d279fba7..4535ca7c5 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -13,60 +13,66 @@ defmodule Pleroma.Pagination do alias Pleroma.Repo @default_limit 20 + @page_keys ["max_id", "min_id", "limit", "since_id", "order"] - def fetch_paginated(query, params, type \\ :keyset) + def page_keys, do: @page_keys - def fetch_paginated(query, %{"total" => true} = params, :keyset) do + def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil) + + def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do total = Repo.aggregate(query, :count, :id) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset) + items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding) } end - def fetch_paginated(query, params, :keyset) do + def fetch_paginated(query, params, :keyset, table_binding) do options = cast_params(params) query - |> paginate(options, :keyset) + |> paginate(options, :keyset, table_binding) |> Repo.all() |> enforce_order(options) end - def fetch_paginated(query, %{"total" => true} = params, :offset) do - total = Repo.aggregate(query, :count, :id) + def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do + total = + query + |> Ecto.Query.exclude(:left_join) + |> Repo.aggregate(:count, :id) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :offset) + items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding) } end - def fetch_paginated(query, params, :offset) do + def fetch_paginated(query, params, :offset, table_binding) do options = cast_params(params) query - |> paginate(options, :offset) + |> paginate(options, :offset, table_binding) |> Repo.all() end - def paginate(query, options, method \\ :keyset) + def paginate(query, options, method \\ :keyset, table_binding \\ nil) - def paginate(query, options, :keyset) do + def paginate(query, options, :keyset, table_binding) do query - |> restrict(:min_id, options) - |> restrict(:since_id, options) - |> restrict(:max_id, options) - |> restrict(:order, options) - |> restrict(:limit, options) + |> restrict(:min_id, options, table_binding) + |> restrict(:since_id, options, table_binding) + |> restrict(:max_id, options, table_binding) + |> restrict(:order, options, table_binding) + |> restrict(:limit, options, table_binding) end - def paginate(query, options, :offset) do + def paginate(query, options, :offset, table_binding) do query - |> restrict(:order, options) - |> restrict(:offset, options) - |> restrict(:limit, options) + |> restrict(:order, options, table_binding) + |> restrict(:offset, options, table_binding) + |> restrict(:limit, options, table_binding) end defp cast_params(params) do @@ -75,7 +81,8 @@ defp cast_params(params) do since_id: :string, max_id: :string, offset: :integer, - limit: :integer + limit: :integer, + skip_order: :boolean } params = @@ -88,38 +95,48 @@ defp cast_params(params) do changeset.changes end - defp restrict(query, :min_id, %{min_id: min_id}) do - where(query, [q], q.id > ^min_id) + defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do + where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id) end - defp restrict(query, :since_id, %{since_id: since_id}) do - where(query, [q], q.id > ^since_id) + defp restrict(query, :since_id, %{since_id: since_id}, table_binding) do + where(query, [{q, table_position(query, table_binding)}], q.id > ^since_id) end - defp restrict(query, :max_id, %{max_id: max_id}) do - where(query, [q], q.id < ^max_id) + defp restrict(query, :max_id, %{max_id: max_id}, table_binding) do + where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id) end - defp restrict(query, :order, %{min_id: _}) do - order_by(query, [u], fragment("? asc nulls last", u.id)) + defp restrict(query, :order, %{skip_order: true}, _), do: query + + defp restrict(query, :order, %{min_id: _}, table_binding) do + order_by( + query, + [{u, table_position(query, table_binding)}], + fragment("? asc nulls last", u.id) + ) end - defp restrict(query, :order, _options) do - order_by(query, [u], fragment("? desc nulls last", u.id)) + defp restrict(query, :order, _options, table_binding) do + order_by( + query, + [{u, table_position(query, table_binding)}], + fragment("? desc nulls last", u.id) + ) end - defp restrict(query, :offset, %{offset: offset}) do + defp restrict(query, :offset, %{offset: offset}, _table_binding) do offset(query, ^offset) end - defp restrict(query, :limit, options) do + defp restrict(query, :limit, options, _table_binding) do limit = Map.get(options, :limit, @default_limit) query |> limit(^limit) end - defp restrict(query, _, _), do: query + defp restrict(query, _, _, _), do: query defp enforce_order(result, %{min_id: _}) do result @@ -127,4 +144,10 @@ defp enforce_order(result, %{min_id: _}) do end defp enforce_order(result, _), do: result + + defp table_position(%Ecto.Query{} = query, binding_name) do + Map.get(query.aliases, binding_name, 0) + end + + defp table_position(_, _), do: 0 end diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index a3278dbef..174a8389c 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do import Plug.Conn import Pleroma.Web.Gettext + alias Pleroma.Config alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug @behaviour Plug @@ -15,6 +16,8 @@ def init(%{scopes: _} = options), do: options def call(%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 @@ -60,6 +63,15 @@ def filter_descendants(scopes, supported_scopes) do ) 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 + defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do if options[:skip_instance_privacy_check] do conn diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index ee808f31f..582fb1f92 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -5,19 +5,38 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do import Pleroma.Web.TranslationHelpers import Plug.Conn + alias Pleroma.User + alias Pleroma.Web.OAuth def init(options) do options end - def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do - conn + def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do + token = assigns[:token] + + cond do + not Pleroma.Config.enforce_oauth_admin_scope_usage?() -> + conn + + token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> + # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. + # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. + conn + + true -> + fail(conn) + end end def call(conn, _) do + fail(conn) + end + + defp fail(conn) do conn - |> render_error(:forbidden, "User is not admin.") - |> halt + |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") + |> halt() end end diff --git a/lib/pleroma/report_note.ex b/lib/pleroma/report_note.ex new file mode 100644 index 000000000..0db86d1a1 --- /dev/null +++ b/lib/pleroma/report_note.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReportNote do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.ReportNote + alias Pleroma.User + + @type t :: %__MODULE__{} + + schema "report_notes" do + field(:content, :string) + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) + + timestamps() + end + + @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t(), String.t()) :: + {:ok, ReportNote.t()} | {:error, Changeset.t()} + def create(user_id, activity_id, content) do + attrs = %{ + user_id: user_id, + activity_id: activity_id, + content: content + } + + %ReportNote{} + |> cast(attrs, [:user_id, :activity_id, :content]) + |> validate_required([:user_id, :activity_id, :content]) + |> Repo.insert() + end + + @spec destroy(FlakeId.Ecto.CompatType.t()) :: + {:ok, ReportNote.t()} | {:error, Changeset.t()} + def destroy(id) do + from(r in ReportNote, where: r.id == ^id) + |> Repo.one() + |> Repo.delete() + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 694f1f110..706aee2ff 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -862,6 +862,13 @@ def get_friends(user, page \\ nil) do |> Repo.all() end + def get_friends_ap_ids(user) do + user + |> get_friends_query(nil) + |> select([u], u.ap_id) + |> Repo.all() + end + def get_friends_ids(user, page \\ nil) do user |> get_friends_query(page) @@ -1136,7 +1143,8 @@ def muted_notifications?(%User{} = user, %User{} = target), def blocks?(nil, _), do: false def blocks?(%User{} = user, %User{} = target) do - blocks_user?(user, target) || blocks_domain?(user, target) + blocks_user?(user, target) || + (!User.following?(user, target) && blocks_domain?(user, target)) end def blocks_user?(%User{} = user, %User{} = target) do @@ -1839,13 +1847,28 @@ defp truncate_field(%{"name" => name, "value" => value}) do end def admin_api_update(user, params) do - user - |> cast(params, [ - :is_moderator, - :is_admin, - :show_role - ]) - |> update_and_set_cache() + changeset = + cast(user, params, [ + :is_moderator, + :is_admin, + :show_role + ]) + + with {:ok, updated_user} <- update_and_set_cache(changeset) do + if user.is_admin && !updated_user.is_admin do + # Tokens & authorizations containing any admin scopes must be revoked (revoking all). + # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins). + global_sign_out(user) + end + + {:ok, updated_user} + end + end + + @doc "Signs user out of all applications" + def global_sign_out(user) do + OAuth.Authorization.delete_user_authorizations(user) + OAuth.Token.delete_user_tokens(user) end def mascot_update(user, url) do diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 6571102a9..16e6b0057 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -950,6 +950,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) domain_blocks = user.domain_blocks || [] + following_ap_ids = User.get_friends_ap_ids(user) + query = if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) @@ -964,8 +966,22 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do activity.data, ^blocked_ap_ids ), - where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks), - where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks) + where: + fragment( + "(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)", + activity.actor, + ^domain_blocks, + activity.actor, + ^following_ap_ids + ), + where: + fragment( + "(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)", + o.data, + ^domain_blocks, + o.data, + ^following_ap_ids + ) ) end @@ -1052,6 +1068,13 @@ defp maybe_preload_bookmarks(query, opts) do |> Activity.with_preloaded_bookmark(opts["user"]) end + defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do + query + |> Activity.with_preloaded_report_notes() + end + + defp maybe_preload_report_notes(query, _), do: query + defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query defp maybe_set_thread_muted_field(query, opts) do @@ -1105,6 +1128,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do Activity |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) + |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) |> restrict_recipients(recipients, opts["user"]) @@ -1141,6 +1165,25 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do |> maybe_update_cc(list_memberships, opts["user"]) end + @doc """ + Fetch favorites activities of user with order by sort adds to favorites + """ + @spec fetch_favourites(User.t(), map(), atom()) :: list(Activity.t()) + def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do + user.ap_id + |> Activity.Queries.by_actor() + |> Activity.Queries.by_type("Like") + |> Activity.with_joined_object() + |> Object.with_joined_activity() + |> select([_like, object, activity], %{activity | object: object}) + |> order_by([like, _, _], desc: like.id) + |> Pagination.fetch_paginated( + Map.merge(params, %{"skip_order" => true}), + pagination, + :object_activity + ) + end + defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) when is_list(list_memberships) and length(list_memberships) > 0 do Enum.map(activities, fn diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 2ca805c09..e87d09134 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -787,6 +787,7 @@ def get_reports(params, page, page_size) do params |> Map.put("type", "Flag") |> Map.put("skip_preload", true) + |> Map.put("preload_report_notes", true) |> Map.put("total", true) |> Map.put("limit", page_size) |> Map.put("offset", (page - 1) * page_size) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index b003d1f35..c8abeff06 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Activity alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.ReportNote alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub @@ -30,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:accounts"]} + %{scopes: ["read:accounts"], admin: true} when action in [:list_users, :user_show, :right_get, :invites] ) plug( OAuthScopesPlug, - %{scopes: ["write:accounts"]} + %{scopes: ["write:accounts"], admin: true} when action in [ :get_invite_token, :revoke_invite, @@ -58,35 +59,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:reports"]} when action in [:list_reports, :report_show] + %{scopes: ["read:reports"], admin: true} + when action in [:list_reports, :report_show] ) plug( OAuthScopesPlug, - %{scopes: ["write:reports"]} + %{scopes: ["write:reports"], admin: true} when action in [:report_update_state, :report_respond] ) plug( OAuthScopesPlug, - %{scopes: ["read:statuses"]} when action == :list_user_statuses + %{scopes: ["read:statuses"], admin: true} + when action == :list_user_statuses ) plug( OAuthScopesPlug, - %{scopes: ["write:statuses"]} + %{scopes: ["write:statuses"], admin: true} when action in [:status_update, :status_delete] ) plug( OAuthScopesPlug, - %{scopes: ["read"]} + %{scopes: ["read"], admin: true} when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] ) plug( OAuthScopesPlug, - %{scopes: ["write"]} + %{scopes: ["write"], admin: true} when action in [:relay_follow, :relay_unfollow, :config_update] ) @@ -238,7 +241,7 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do }) conn - |> put_view(StatusView) + |> put_view(Pleroma.Web.AdminAPI.StatusView) |> render("index.json", %{activities: activities, as: :activity}) end @@ -641,9 +644,11 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic def list_reports(conn, params) do {page, page_size} = page_params(params) + reports = Utils.get_reports(params, page, page_size) + conn |> put_view(ReportView) - |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)}) + |> render("index.json", %{reports: reports}) end def list_grouped_reports(conn, _params) do @@ -687,32 +692,39 @@ def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) d end end - def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do - with false <- is_nil(params["status"]), - %Activity{} <- Activity.get_by_id(id) do - params = - params - |> Map.put("in_reply_to_status_id", id) - |> Map.put("visibility", "direct") - - {:ok, activity} = CommonAPI.post(user, params) - + def report_notes_create(%{assigns: %{user: user}} = conn, %{ + "id" => report_id, + "content" => content + }) do + with {:ok, _} <- ReportNote.create(user.id, report_id, content) do ModerationLog.insert_log(%{ - action: "report_response", + action: "report_note", actor: user, - subject: activity, - text: params["status"] + subject: Activity.get_by_id(report_id), + text: content }) - conn - |> put_view(StatusView) - |> render("show.json", %{activity: activity}) + json_response(conn, :no_content, "") else - true -> - {:param_cast, nil} + _ -> json_response(conn, :bad_request, "") + end + end - nil -> - {:error, :not_found} + def report_notes_delete(%{assigns: %{user: user}} = conn, %{ + "id" => note_id, + "report_id" => report_id + }) do + with {:ok, note} <- ReportNote.destroy(note_id) do + ModerationLog.insert_log(%{ + action: "report_note_delete", + actor: user, + subject: Activity.get_by_id(report_id), + text: note.content + }) + + json_response(conn, :no_content, "") + else + _ -> json_response(conn, :bad_request, "") end end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 13602efd9..4880d2992 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -39,7 +39,8 @@ def render("show.json", %{report: report, user: user, account: account, statuses content: content, created_at: created_at, statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), - state: report.data["state"] + state: report.data["state"], + notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) } end @@ -69,6 +70,28 @@ def render("index_grouped.json", %{groups: groups}) do } end + def render("index_notes.json", %{notes: notes}) when is_list(notes) do + Enum.map(notes, &render(__MODULE__, "show_note.json", &1)) + end + + def render("index_notes.json", _), do: [] + + def render("show_note.json", %{ + id: id, + content: content, + user_id: user_id, + inserted_at: inserted_at + }) do + user = User.get_by_id(user_id) + + %{ + id: id, + content: content, + user: merge_account_views(user), + created_at: Utils.to_masto_date(inserted_at) + } + end + defp merge_account_views(%User{} = user) do Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) diff --git a/lib/pleroma/web/admin_api/views/status_view.ex b/lib/pleroma/web/admin_api/views/status_view.ex new file mode 100644 index 000000000..6f2b2b09c --- /dev/null +++ b/lib/pleroma/web/admin_api/views/status_view.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.StatusView do + use Pleroma.Web, :view + + require Pleroma.Constants + + alias Pleroma.User + + def render("index.json", opts) do + render_many(opts.activities, __MODULE__, "show.json", opts) + end + + def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do + user = get_user(activity.data["actor"]) + + Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts) + |> Map.merge(%{account: merge_account_views(user)}) + end + + defp merge_account_views(%User{} = user) do + Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) + |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) + end + + defp merge_account_views(_), do: %{} + + defp get_user(ap_id) do + cond do + user = User.get_cached_by_ap_id(ap_id) -> + user + + user = User.get_by_guessed_nickname(ap_id) -> + user + + true -> + User.error_user(ap_id) + end + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 74b223cf4..1149fb469 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -346,15 +346,11 @@ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do @doc "GET /api/v1/favourites" def favourites(%{assigns: %{user: user}} = conn, params) do - params = - params - |> Map.put("type", "Create") - |> Map.put("favorited_by", user.ap_id) - |> Map.put("blocking_user", user) - activities = - ActivityPub.fetch_activities([], params) - |> Enum.reverse() + ActivityPub.fetch_favourites( + user, + Map.take(params, Pleroma.Pagination.page_keys()) + ) conn |> add_link_headers(activities) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 2aee8cab2..87acdec97 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -222,7 +222,7 @@ def token_exchange( {:user_active, true} <- {:user_active, !user.deactivated}, {:password_reset_pending, false} <- {:password_reset_pending, user.password_reset_pending}, - {:ok, scopes} <- validate_scopes(app, params), + {:ok, scopes} <- validate_scopes(app, params, user), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, Token.Response.build(user, token)) @@ -471,7 +471,7 @@ defp do_create_authorization( {: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), - {:ok, scopes} <- validate_scopes(app, auth_attrs), + {:ok, scopes} <- validate_scopes(app, auth_attrs, user), {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do Authorization.create_authorization(app, user, scopes) end @@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) - @spec validate_scopes(App.t(), map()) :: + @spec validate_scopes(App.t(), map(), User.t()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(app, params) do + defp validate_scopes(%App{} = app, params, %User{} = user) do params |> Scopes.fetch_scopes(app.scopes) - |> Scopes.validate(app.scopes) + |> Scopes.validate(app.scopes, user) end def default_redirect_uri(%App{} = app) do diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 48bd14407..00da225b9 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do Functions for dealing with scopes. """ + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User + @doc """ Fetch scopes from request params. @@ -53,15 +56,38 @@ def to_string(scopes), do: Enum.join(scopes, " ") @doc """ Validates scopes. """ - @spec validate(list() | nil, list()) :: + @spec validate(list() | nil, list(), User.t()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate([], _app_scopes), do: {:error, :missing_scopes} - def validate(nil, _app_scopes), do: {:error, :missing_scopes} + def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []], + do: {:error, :missing_scopes} - def validate(scopes, app_scopes) do - case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do + def validate(scopes, app_scopes, %User{} = user) do + with {:ok, _} <- ensure_scopes_support(scopes, app_scopes), + {:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do + {:ok, scopes} + end + end + + defp ensure_scopes_support(scopes, app_scopes) do + case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do ^scopes -> {:ok, scopes} _ -> {:error, :unsupported_scopes} end end + + defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do + if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do + {:ok, scopes} + else + # Gracefully dropping admin scopes from requested scopes if user isn't an admin (not raising) + scopes = scopes -- OAuthScopesPlug.filter_descendants(scopes, ["admin"]) + validate(scopes, app_scopes, user) + end + end + + def contains_admin_scopes?(scopes) do + scopes + |> OAuthScopesPlug.filter_descendants(["admin"]) + |> Enum.any?() + end end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index a474d41d4..69dfa92e3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( OAuthScopesPlug, - %{scopes: ["write"]} + %{scopes: ["write"], admin: true} when action in [ :create, :delete, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e6c4f6f14..f6c128283 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -187,7 +187,8 @@ defmodule Pleroma.Web.Router do get("/grouped_reports", AdminAPIController, :list_grouped_reports) get("/reports/:id", AdminAPIController, :report_show) patch("/reports", AdminAPIController, :reports_update) - post("/reports/:id/respond", AdminAPIController, :report_respond) + post("/reports/:id/notes", AdminAPIController, :report_notes_create) + delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete) put("/statuses/:id", AdminAPIController, :status_update) delete("/statuses/:id", AdminAPIController, :status_delete) @@ -528,7 +529,10 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/feed", Feed.FeedController, :feed) get("/users/:nickname", Feed.FeedController, :feed_redirect) + end + scope "/", Pleroma.Web do + pipe_through(:browser) get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end diff --git a/mix.exs b/mix.exs index 7c8e52a67..f1a62f6e3 100644 --- a/mix.exs +++ b/mix.exs @@ -163,6 +163,9 @@ defp deps do {:remote_ip, git: "https://git.pleroma.social/pleroma/remote_ip.git", ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"}, + {:captcha, + git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", + ref: "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2"}, {:mox, "~> 0.5", only: :test} ] ++ oauth_deps() end diff --git a/mix.lock b/mix.lock index b008f2b55..401bcdb6e 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2", [ref: "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2"]}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, @@ -36,8 +37,8 @@ "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, "excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"}, - "fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "fast_html": {:hex, :fast_html, "1.0.1", "5bc7df4dc4607ec2c314c16414e4111d79a209956c4f5df96602d194c61197f9", [:make, :mix], [], "hexpm"}, + "fast_sanitize": {:hex, :fast_sanitize, "0.1.6", "60a5ae96879956dea409a91a77f5dd2994c24cc10f80eefd8f9892ee4c0c7b25", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"}, diff --git a/priv/repo/migrations/20191203043610_create_report_notes.exs b/priv/repo/migrations/20191203043610_create_report_notes.exs new file mode 100644 index 000000000..a4f8c096d --- /dev/null +++ b/priv/repo/migrations/20191203043610_create_report_notes.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.CreateReportNotes do + use Ecto.Migration + + def change do + create_if_not_exists table(:report_notes) do + add(:user_id, references(:users, type: :uuid)) + add(:activity_id, references(:activities, type: :uuid)) + add(:content, :string) + + timestamps() + end + end +end diff --git a/priv/static/index.html b/priv/static/index.html index 2a6c7b036..2467aa22a 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/static/css/app.fd71461124f3eb029b1b.css b/priv/static/static/css/app.ae04505b31bb0ee2765e.css similarity index 97% rename from priv/static/static/css/app.fd71461124f3eb029b1b.css rename to priv/static/static/css/app.ae04505b31bb0ee2765e.css index 6ab9849ec..05e3f2087 100644 Binary files a/priv/static/static/css/app.fd71461124f3eb029b1b.css and b/priv/static/static/css/app.ae04505b31bb0ee2765e.css differ diff --git a/priv/static/static/css/app.fd71461124f3eb029b1b.css.map b/priv/static/static/css/app.ae04505b31bb0ee2765e.css.map similarity index 95% rename from priv/static/static/css/app.fd71461124f3eb029b1b.css.map rename to priv/static/static/css/app.ae04505b31bb0ee2765e.css.map index 660b09d2c..72f33316e 100644 --- a/priv/static/static/css/app.fd71461124f3eb029b1b.css.map +++ b/priv/static/static/css/app.ae04505b31bb0ee2765e.css.map @@ -1 +1 @@ -{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.fd71461124f3eb029b1b.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.ae04505b31bb0ee2765e.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/font/LICENSE.txt b/priv/static/static/font/LICENSE.txt deleted file mode 100755 index 95966f00e..000000000 --- a/priv/static/static/font/LICENSE.txt +++ /dev/null @@ -1,39 +0,0 @@ -Font license info - - -## Font Awesome - - Copyright (C) 2016 by Dave Gandy - - Author: Dave Gandy - License: SIL () - Homepage: http://fortawesome.github.com/Font-Awesome/ - - -## Entypo - - Copyright (C) 2012 by Daniel Bruce - - Author: Daniel Bruce - License: SIL (http://scripts.sil.org/OFL) - Homepage: http://www.entypo.com - - -## Iconic - - Copyright (C) 2012 by P.J. Onori - - Author: P.J. Onori - License: SIL (http://scripts.sil.org/OFL) - Homepage: http://somerandomdude.com/work/iconic/ - - -## Fontelico - - Copyright (C) 2012 by Fontello project - - Author: Crowdsourced, for Fontello project - License: SIL (http://scripts.sil.org/OFL) - Homepage: http://fontello.com - - diff --git a/priv/static/static/font/README.txt b/priv/static/static/font/README.txt deleted file mode 100755 index beaab3366..000000000 --- a/priv/static/static/font/README.txt +++ /dev/null @@ -1,75 +0,0 @@ -This webfont is generated by http://fontello.com open source project. - - -================================================================================ -Please, note, that you should obey original font licenses, used to make this -webfont pack. Details available in LICENSE.txt file. - -- Usually, it's enough to publish content of LICENSE.txt file somewhere on your - site in "About" section. - -- If your project is open-source, usually, it will be ok to make LICENSE.txt - file publicly available in your repository. - -- Fonts, used in Fontello, don't require a clickable link on your site. - But any kind of additional authors crediting is welcome. -================================================================================ - - -Comments on archive content ---------------------------- - -- /font/* - fonts in different formats - -- /css/* - different kinds of css, for all situations. Should be ok with - twitter bootstrap. Also, you can skip style and assign icon classes - directly to text elements, if you don't mind about IE7. - -- demo.html - demo file, to show your webfont content - -- LICENSE.txt - license info about source fonts, used to build your one. - -- config.json - keeps your settings. You can import it back into fontello - anytime, to continue your work - - -Why so many CSS files ? ------------------------ - -Because we like to fit all your needs :) - -- basic file, .css - is usually enough, it contains @font-face - and character code definitions - -- *-ie7.css - if you need IE7 support, but still don't wish to put char codes - directly into html - -- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face - rules, but still wish to benefit from css generation. That can be very - convenient for automated asset build systems. When you need to update font - - no need to manually edit files, just override old version with archive - content. See fontello source code for examples. - -- *-embedded.css - basic css file, but with embedded WOFF font, to avoid - CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. - We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` - server headers. But if you ok with dirty hack - this file is for you. Note, - that data url moved to separate @font-face to avoid problems with - - - - - - - -
-

fontello font demo

- -
-
-
-
icon-cancel0xe800
-
icon-upload0xe801
-
icon-star0xe802
-
icon-star-empty0xe803
-
-
-
icon-retweet0xe804
-
icon-eye-off0xe805
-
icon-search0xe806
-
icon-cog0xe807
-
-
-
icon-logout0xe808
-
icon-down-open0xe809
-
icon-attach0xe80a
-
icon-picture0xe80b
-
-
-
icon-video0xe80c
-
icon-right-open0xe80d
-
icon-left-open0xe80e
-
icon-up-open0xe80f
-
-
-
icon-bell-ringing-o0xe810
-
icon-lock0xe811
-
icon-globe0xe812
-
icon-brush0xe813
-
-
-
icon-attention0xe814
-
icon-plus0xe815
-
icon-adjust0xe816
-
icon-edit0xe817
-
-
-
icon-pencil0xe818
-
icon-pin0xe819
-
icon-wrench0xe81a
-
icon-chart-bar0xe81b
-
-
-
icon-zoom-in0xe81c
-
icon-spin30xe832
-
icon-spin40xe834
-
icon-link-ext0xf08e
-
-
-
icon-link-ext-alt0xf08f
-
icon-menu0xf0c9
-
icon-mail-alt0xf0e0
-
icon-gauge0xf0e4
-
-
-
icon-comment-empty0xf0e5
-
icon-bell-alt0xf0f3
-
icon-plus-squared0xf0fe
-
icon-reply0xf112
-
-
-
icon-smile0xf118
-
icon-lock-open-alt0xf13e
-
icon-ellipsis0xf141
-
icon-play-circled0xf144
-
-
-
icon-thumbs-up-alt0xf164
-
icon-binoculars0xf1e5
-
icon-user-plus0xf234
-
-
- - - \ No newline at end of file diff --git a/priv/static/static/font/font/fontello.woff2 b/priv/static/static/font/font/fontello.woff2 deleted file mode 100755 index 078991eb8..000000000 Binary files a/priv/static/static/font/font/fontello.woff2 and /dev/null differ diff --git a/priv/static/static/font/font/fontello.eot b/priv/static/static/font/fontello.1576166651574.eot old mode 100755 new mode 100644 similarity index 99% rename from priv/static/static/font/font/fontello.eot rename to priv/static/static/font/fontello.1576166651574.eot index 1703fd97f..fb27d4037 Binary files a/priv/static/static/font/font/fontello.eot and b/priv/static/static/font/fontello.1576166651574.eot differ diff --git a/priv/static/static/font/font/fontello.svg b/priv/static/static/font/fontello.1576166651574.svg similarity index 100% rename from priv/static/static/font/font/fontello.svg rename to priv/static/static/font/fontello.1576166651574.svg diff --git a/priv/static/static/font/font/fontello.ttf b/priv/static/static/font/fontello.1576166651574.ttf old mode 100755 new mode 100644 similarity index 99% rename from priv/static/static/font/font/fontello.ttf rename to priv/static/static/font/fontello.1576166651574.ttf index e9ed78031..c49743ec6 Binary files a/priv/static/static/font/font/fontello.ttf and b/priv/static/static/font/fontello.1576166651574.ttf differ diff --git a/priv/static/static/font/font/fontello.woff b/priv/static/static/font/fontello.1576166651574.woff old mode 100755 new mode 100644 similarity index 98% rename from priv/static/static/font/font/fontello.woff rename to priv/static/static/font/fontello.1576166651574.woff index 1d5025d3c..bbffd6413 Binary files a/priv/static/static/font/font/fontello.woff and b/priv/static/static/font/fontello.1576166651574.woff differ diff --git a/priv/static/static/font/fontello.1576166651574.woff2 b/priv/static/static/font/fontello.1576166651574.woff2 new file mode 100644 index 000000000..d35dce862 Binary files /dev/null and b/priv/static/static/font/fontello.1576166651574.woff2 differ diff --git a/priv/static/static/fontello.1576166651574.css b/priv/static/static/fontello.1576166651574.css new file mode 100644 index 000000000..54f9fe05f Binary files /dev/null and b/priv/static/static/fontello.1576166651574.css differ diff --git a/priv/static/static/font/config.json b/priv/static/static/fontello.json similarity index 100% rename from priv/static/static/font/config.json rename to priv/static/static/fontello.json diff --git a/priv/static/static/js/app.4ab7097a5650339b9e3d.js b/priv/static/static/js/app.4ab7097a5650339b9e3d.js deleted file mode 100644 index 33141e412..000000000 Binary files a/priv/static/static/js/app.4ab7097a5650339b9e3d.js and /dev/null differ diff --git a/priv/static/static/js/app.4ab7097a5650339b9e3d.js.map b/priv/static/static/js/app.4ab7097a5650339b9e3d.js.map deleted file mode 100644 index b47e90c09..000000000 Binary files a/priv/static/static/js/app.4ab7097a5650339b9e3d.js.map and /dev/null differ diff --git a/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js b/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js new file mode 100644 index 000000000..124f284be Binary files /dev/null and b/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js differ diff --git a/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js.map b/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js.map new file mode 100644 index 000000000..7c369185e Binary files /dev/null and b/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js.map differ diff --git a/priv/static/static/js/app.d20ca27d22d74eb7bce0.js b/priv/static/static/js/app.d20ca27d22d74eb7bce0.js deleted file mode 100644 index 7abf2ec28..000000000 Binary files a/priv/static/static/js/app.d20ca27d22d74eb7bce0.js and /dev/null differ diff --git a/priv/static/static/js/app.d20ca27d22d74eb7bce0.js.map b/priv/static/static/js/app.d20ca27d22d74eb7bce0.js.map deleted file mode 100644 index 6c96ca5b2..000000000 Binary files a/priv/static/static/js/app.d20ca27d22d74eb7bce0.js.map and /dev/null differ diff --git a/priv/static/static/js/vendors~app.76db8e4cdf29decd5cab.js b/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js similarity index 95% rename from priv/static/static/js/vendors~app.76db8e4cdf29decd5cab.js rename to priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js index 135bdebb3..a64eee9a9 100644 Binary files a/priv/static/static/js/vendors~app.76db8e4cdf29decd5cab.js and b/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js differ diff --git a/priv/static/static/js/vendors~app.76db8e4cdf29decd5cab.js.map b/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js.map similarity index 78% rename from priv/static/static/js/vendors~app.76db8e4cdf29decd5cab.js.map rename to priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js.map index 6513c0a0b..2e88b3ce2 100644 Binary files a/priv/static/static/js/vendors~app.76db8e4cdf29decd5cab.js.map and b/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js.map differ diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js index 276af8173..4738f3391 100644 Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ diff --git a/priv/static/sw.js b/priv/static/sw.js index c2de0cfe0..5605bb05e 100644 Binary files a/priv/static/sw.js and b/priv/static/sw.js differ diff --git a/test/captcha_test.exs b/test/captcha_test.exs index 9f395d6b4..393c8219e 100644 --- a/test/captcha_test.exs +++ b/test/captcha_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.CaptchaTest do import Tesla.Mock alias Pleroma.Captcha.Kocaptcha + alias Pleroma.Captcha.Native @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] @@ -43,4 +44,21 @@ test "new and validate" do ) == :ok end end + + describe "Native" do + test "new and validate" do + new = Native.new() + + assert %{ + answer_data: answer, + token: token, + type: :native, + url: "data:image/png;base64," <> _ + } = new + + assert is_binary(answer) + assert :ok = Native.validate(token, answer, answer) + assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar") + end + end end diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs index 4240f6a65..f2168b735 100644 --- a/test/moderation_log_test.exs +++ b/test/moderation_log_test.exs @@ -214,7 +214,7 @@ test "logging report response", %{moderator: moderator} do {:ok, _} = ModerationLog.insert_log(%{ actor: moderator, - action: "report_response", + action: "report_note", subject: report, text: "look at this" }) @@ -222,7 +222,7 @@ test "logging report response", %{moderator: moderator} do log = Repo.one(ModerationLog) assert log.data["message"] == - "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}" + "@#{moderator.nickname} added note 'look at this' to report ##{report.id}" end test "logging status sensitivity update", %{moderator: moderator} do diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index be6d1340b..89f32f43a 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -224,4 +224,42 @@ test "filters scopes which directly match or are ancestors of supported scopes" assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] end end + + describe "transform_scopes/2" do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) + + setup do + {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}} + end + + test "with :admin option, prefixes all requested scopes with `admin:` " <> + "and [optionally] keeps only prefixed scopes, " <> + "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting", + %{f: f} do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + + assert f.(["read"], %{admin: true}) == ["admin:read", "read"] + + assert f.(["read", "write"], %{admin: true}) == [ + "admin:read", + "read", + "admin:write", + "write" + ] + + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + + assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"] + + assert f.(["read", "write:reports"], %{admin: true}) == [ + "admin:read", + "admin:write:reports" + ] + end + + test "with no supported options, returns unmodified scopes", %{f: f} do + assert f.(["read"], %{}) == ["read"] + assert f.(["read", "write"], %{}) == ["read", "write"] + end + end end diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 136dcc54e..bc6fcd73c 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -8,36 +8,116 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do alias Pleroma.Plugs.UserIsAdminPlug import Pleroma.Factory - test "accepts a user that is admin" do - user = insert(:user, is_admin: true) + describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end - conn = - build_conn() - |> assign(:user, user) + test "accepts a user that is an admin" do + user = insert(:user, is_admin: true) - ret_conn = - conn - |> UserIsAdminPlug.call(%{}) + conn = assign(build_conn(), :user, user) - assert conn == ret_conn + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "denies a user that isn't an admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "denies when a user isn't set" do + conn = UserIsAdminPlug.call(build_conn(), %{}) + + assert conn.status == 403 + end end - test "denies a user that isn't admin" do - user = insert(:user) + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + end - conn = - build_conn() - |> assign(:user, user) - |> UserIsAdminPlug.call(%{}) + setup do + admin_user = insert(:user, is_admin: true) + non_admin_user = insert(:user, is_admin: false) + blank_user = nil - assert conn.status == 403 - end + {:ok, %{users: [admin_user, non_admin_user, blank_user]}} + end - test "denies when a user isn't set" do - conn = - build_conn() - |> UserIsAdminPlug.call(%{}) + test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do + user = insert(:user, is_admin: true) + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) - assert conn.status == 403 + conn = + conn + |> assign(:user, user) + |> assign(:token, token) + + ret_conn = UserIsAdminPlug.call(conn, %{}) + + assert conn == ret_conn + end + + test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do + user = insert(:user, is_admin: false) + token = insert(:oauth_token, user: user, scopes: ["admin:something"]) + + conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do + token = insert(:oauth_token, scopes: ["admin:something"]) + + conn = + conn + |> assign(:user, nil) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + + test "if token lacks admin scopes, denies users regardless of is_admin flag", + %{users: users} do + for user <- users do + token = insert(:oauth_token, user: user) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + end + + test "if token is missing, denies users regardless of is_admin flag", %{users: users} do + for user <- users do + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, nil) + |> UserIsAdminPlug.call(%{}) + + assert conn.status == 403 + end + end end end diff --git a/test/user_test.exs b/test/user_test.exs index bfa8faafa..d7ab63463 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -914,6 +914,16 @@ test "unblocks domains" do refute User.blocks?(user, collateral_user) end + + test "follows take precedence over domain blocks" do + user = insert(:user) + good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"}) + + {:ok, user} = User.block_domain(user, "meanies.social") + {:ok, user} = User.follow(user, good_eggo) + + refute User.blocks?(user, good_eggo) + end end describe "blocks_import" do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 97844a407..ad1fb6d02 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -608,6 +608,39 @@ test "doesn't return activities from blocked domains" do refute repeat_activity in activities end + test "does return activities from followed users on blocked domains" do + domain = "meanies.social" + domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) + blocker = insert(:user) + + {:ok, blocker} = User.follow(blocker, domain_user) + {:ok, blocker} = User.block_domain(blocker, domain) + + assert User.following?(blocker, domain_user) + assert User.blocks_domain?(blocker, domain_user) + refute User.blocks?(blocker, domain_user) + + note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) + activity = insert(:note_activity, %{note: note}) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true}) + + assert activity in activities + + # And check that if the guy we DO follow boosts someone else from their domain, + # that should be hidden + another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"}) + bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}}) + bad_activity = insert(:note_activity, %{note: bad_note}) + {:ok, repeat_activity, _} = CommonAPI.repeat(bad_activity.id, domain_user) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true}) + + refute repeat_activity in activities + end + test "doesn't return muted activities" do activity_one = insert(:note_activity) activity_two = insert(:note_activity) @@ -1592,6 +1625,38 @@ test "detects hidden follows/followers for friendica" do end end + describe "fetch_favourites/3" do + test "returns a favourite activities sorted by adds to favorite" do + user = insert(:user) + other_user = insert(:user) + user1 = insert(:user) + user2 = insert(:user) + {:ok, a1} = CommonAPI.post(user1, %{"status" => "bla"}) + {:ok, _a2} = CommonAPI.post(user2, %{"status" => "traps are happy"}) + {:ok, a3} = CommonAPI.post(user2, %{"status" => "Trees Are "}) + {:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "}) + {:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "}) + + {:ok, _, _} = CommonAPI.favorite(a4.id, user) + {:ok, _, _} = CommonAPI.favorite(a3.id, other_user) + Process.sleep(1000) + {:ok, _, _} = CommonAPI.favorite(a3.id, user) + {:ok, _, _} = CommonAPI.favorite(a5.id, other_user) + Process.sleep(1000) + {:ok, _, _} = CommonAPI.favorite(a5.id, user) + {:ok, _, _} = CommonAPI.favorite(a4.id, other_user) + Process.sleep(1000) + {:ok, _, _} = CommonAPI.favorite(a1.id, user) + {:ok, _, _} = CommonAPI.favorite(a1.id, other_user) + result = ActivityPub.fetch_favourites(user) + + assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] + + result = ActivityPub.fetch_favourites(user, %{"limit" => 2}) + assert Enum.map(result, & &1.id) == [a1.id, a5.id] + end + end + describe "Move activity" do test "create" do %{ap_id: old_ap_id} = old_user = insert(:user) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 4148f04bc..49ff005b6 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.HTML alias Pleroma.ModerationLog alias Pleroma.Repo + alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.UserInviteToken @@ -25,6 +26,60 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do :ok end + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end + + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) + end + + test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do + user = insert(:user) + admin = insert(:user, is_admin: true) + url = "/api/pleroma/admin/users/#{user.nickname}" + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, 200) + end + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + end + end + describe "DELETE /api/pleroma/admin/users" do test "single user" do admin = insert(:user, is_admin: true) @@ -98,7 +153,7 @@ test "Create" do assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] end - test "Cannot create user with exisiting email" do + test "Cannot create user with existing email" do admin = insert(:user, is_admin: true) user = insert(:user) @@ -129,7 +184,7 @@ test "Cannot create user with exisiting email" do ] end - test "Cannot create user with exisiting nickname" do + test "Cannot create user with existing nickname" do admin = insert(:user, is_admin: true) user = insert(:user) @@ -1560,7 +1615,8 @@ test "returns 403 when requested by a non-admin" do |> assign(:user, user) |> get("/api/pleroma/admin/reports") - assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} + assert json_response(conn, :forbidden) == + %{"error" => "User is not an admin or OAuth admin scope is not granted."} end test "returns 403 when requested by anonymous" do @@ -1776,61 +1832,6 @@ test "account not empty if status was deleted", %{ end end - describe "POST /api/pleroma/admin/reports/:id/respond" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin), admin: admin} - end - - test "returns created dm", %{conn: conn, admin: admin} do - [reporter, target_user] = insert_pair(:user) - activity = insert(:note_activity, user: target_user) - - {:ok, %{id: report_id}} = - CommonAPI.report(reporter, %{ - "account_id" => target_user.id, - "comment" => "I feel offended", - "status_ids" => [activity.id] - }) - - response = - conn - |> post("/api/pleroma/admin/reports/#{report_id}/respond", %{ - "status" => "I will check it out" - }) - |> json_response(:ok) - - recipients = Enum.map(response["mentions"], & &1["username"]) - - assert reporter.nickname in recipients - assert response["content"] == "I will check it out" - assert response["visibility"] == "direct" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} responded with 'I will check it out' to report ##{ - response["id"] - }" - end - - test "returns 400 when status is missing", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/reports/test/respond") - - assert json_response(conn, :bad_request) == "Invalid parameters" - end - - test "returns 404 when report id is invalid", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/reports/test/respond", %{ - "status" => "foo" - }) - - assert json_response(conn, :not_found) == "Not found" - end - end - describe "PUT /api/pleroma/admin/statuses/:id" do setup %{conn: conn} do admin = insert(:user, is_admin: true) @@ -3027,6 +3028,77 @@ test "it resend emails for two users", %{admin: admin} do }" end end + + describe "POST /reports/:id/notes" do + setup do + admin = insert(:user, is_admin: true) + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + "account_id" => target_user.id, + "comment" => "I feel offended", + "status_ids" => [activity.id] + }) + + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting!" + }) + + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting2!" + }) + + %{ + admin_id: admin.id, + report_id: report_id, + admin: admin + } + end + + test "it creates report note", %{admin_id: admin_id, report_id: report_id} do + [note, _] = Repo.all(ReportNote) + + assert %{ + activity_id: ^report_id, + content: "this is disgusting!", + user_id: ^admin_id + } = note + end + + test "it returns reports with notes", %{admin: admin} do + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/reports") + + response = json_response(conn, 200) + notes = hd(response["reports"])["notes"] + [note, _] = notes + + assert note["user"]["nickname"] == admin.nickname + assert note["content"] == "this is disgusting!" + assert note["created_at"] + assert response["total"] == 1 + end + + test "it deletes the note", %{admin: admin, report_id: report_id} do + assert ReportNote |> Repo.all() |> length() == 2 + + [note, _] = Repo.all(ReportNote) + + build_conn() + |> assign(:user, admin) + |> delete("/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") + + assert ReportNote |> Repo.all() |> length() == 1 + end + end end # Needed for testing diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs index ef4a806e4..a0c6eab3c 100644 --- a/test/web/admin_api/views/report_view_test.exs +++ b/test/web/admin_api/views/report_view_test.exs @@ -30,6 +30,7 @@ test "renders a report" do Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user}) ), statuses: [], + notes: [], state: "open", id: activity.id } @@ -65,6 +66,7 @@ test "includes reported statuses" do ), statuses: [StatusView.render("show.json", %{activity: activity})], state: "open", + notes: [], id: report_activity.id } diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index beb995cd8..901f2ae41 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -567,33 +567,41 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with end describe "POST /oauth/authorize" do - test "redirects with oauth authorization" do - user = insert(:user) - app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + test "redirects with oauth authorization, " <> + "keeping only non-admin scopes for non-admin user" do + app = insert(:oauth_app, scopes: ["read", "write", "admin"]) redirect_uri = OAuthController.default_redirect_uri(app) - conn = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "scope" => "read:subscope write", - "state" => "statepassed" - } - }) + non_admin = insert(:user, is_admin: false) + admin = insert(:user, is_admin: true) - target = redirected_to(conn) - assert target =~ redirect_uri + for {user, expected_scopes} <- %{ + non_admin => ["read:subscope", "write"], + admin => ["read:subscope", "write", "admin"] + } do + conn = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "scope" => "read:subscope write admin", + "state" => "statepassed" + } + }) - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + target = redirected_to(conn) + assert target =~ redirect_uri - assert %{"state" => "statepassed", "code" => code} = query - auth = Repo.get_by(Authorization, token: code) - assert auth - assert auth.scopes == ["read:subscope", "write"] + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + + assert %{"state" => "statepassed", "code" => code} = query + auth = Repo.get_by(Authorization, token: code) + assert auth + assert auth.scopes == expected_scopes + end end test "returns 401 for wrong credentials", %{conn: conn} do @@ -623,31 +631,34 @@ test "returns 401 for wrong credentials", %{conn: conn} do assert result =~ "Invalid Username/Password" end - test "returns 401 for missing scopes", %{conn: conn} do - user = insert(:user) - app = insert(:oauth_app) + test "returns 401 for missing scopes " <> + "(including all admin-only scopes for non-admin user)" do + user = insert(:user, is_admin: false) + app = insert(:oauth_app, scopes: ["read", "write", "admin"]) redirect_uri = OAuthController.default_redirect_uri(app) - result = - conn - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => "" - } - }) - |> html_response(:unauthorized) + for scope_param <- ["", "admin:read admin:write"] do + result = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => scope_param + } + }) + |> html_response(:unauthorized) - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri - # Error message - assert result =~ "This action is outside the authorized scopes" + # Error message + assert result =~ "This action is outside the authorized scopes" + end end test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do