diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc99b81ee..f9745122a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,6 +52,7 @@ unit-testing: - mix ecto.create - mix ecto.migrate - mix test --trace --preload-modules + - mix coveralls lint: stage: test diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e849285..12c439135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,18 +10,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc. - [Prometheus](https://prometheus.io/) metrics - Support for Mastodon's remote interaction +- Mix Tasks: `mix pleroma.database bump_all_conversations` - Mix Tasks: `mix pleroma.database remove_embedded_objects` +- Mix Tasks: `mix pleroma.database update_users_following_followers_counts` +- Mix Tasks: `mix pleroma.user toggle_confirmed` - Federation: Support for reports - Configuration: `safe_dm_mentions` option - Configuration: `link_name` option - Configuration: `fetch_initial_posts` option - Configuration: `notify_email` option - Configuration: Media proxy `whitelist` option +- Configuration: `report_uri` option - Pleroma API: User subscriptions - Pleroma API: Healthcheck endpoint - Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for making users follow/unfollow each other - Admin API: added filters (role, tags, email, name) for users endpoint +- Admin API: Endpoints for managing reports +- Admin API: Endpoints for deleting and changing the scope of individual reported statuses - AdminFE: initial release with basic user management accessible at /pleroma/admin/ - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) @@ -98,7 +104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`] ## Removed -- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations` +- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations` ## [0.9.9999] - 2019-04-05 ### Security diff --git a/config/config.exs b/config/config.exs index 92d3e0dcd..61e2648a9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -406,8 +406,7 @@ mailer: 10, transmogrifier: 20, scheduled_activities: 10, - background: 5, - user: 10 + background: 5 config :pleroma, :fetch_initial_posts, enabled: false, @@ -466,6 +465,9 @@ token_expires_in: 600, issue_new_refresh_token: true +config :http_signatures, + adapter: Pleroma.Signature + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/test.exs b/config/test.exs index a0c90c371..40db66170 100644 --- a/config/test.exs +++ b/config/test.exs @@ -61,6 +61,8 @@ config :pleroma, :app_account_creation, max_requests: 5 +config :pleroma, :http_security, report_uri: "https://endpoint.com" + try do import_config "test.secret.exs" rescue diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 75fa2ee83..b45c5e285 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -24,7 +24,7 @@ Authentication is required and the user must be an admin. - Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com` - Response: -```JSON +```json { "page_size": integer, "count": integer, @@ -92,7 +92,7 @@ Authentication is required and the user must be an admin. - `nickname` - Response: User’s object -```JSON +```json { "deactivated": bool, "id": integer, @@ -106,15 +106,15 @@ Authentication is required and the user must be an admin. - Method: `PUT` - Params: - - `nickname` - - `tags` + - `nicknames` (array) + - `tags` (array) ### Untag a list of users - Method: `DELETE` - Params: - - `nickname` - - `tags` + - `nicknames` (array) + - `tags` (array) ## `/api/pleroma/admin/users/:nickname/permission_group` @@ -124,7 +124,7 @@ Authentication is required and the user must be an admin. - Params: none - Response: -```JSON +```json { "is_moderator": bool, "is_admin": bool @@ -141,7 +141,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Params: none - Response: -```JSON +```json { "is_moderator": bool, "is_admin": bool @@ -223,7 +223,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Params: none - Response: -```JSON +```json { "invites": [ @@ -250,7 +250,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `token` - Response: -```JSON +```json { "id": integer, "token": string, @@ -280,3 +280,280 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Methods: `GET` - Params: none - Response: password reset token (base64 string) + +## `/api/pleroma/admin/reports` +### Get a list of reports +- Method `GET` +- Params: + - `state`: optional, the state of reports. Valid values are `open`, `closed` and `resolved` + - `limit`: optional, the number of records to retrieve + - `since_id`: optional, returns results that are more recent than the specified id + - `max_id`: optional, returns results that are older than the specified id +- Response: + - On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin + - On success: JSON, returns a list of reports, where: + - `account`: the user who has been reported + - `actor`: the user who has sent the report + - `statuses`: list of statuses that have been included to the report + +```json +{ + "reports": [ + { + "account": { + "acct": "user", + "avatar": "https://pleroma.example.org/images/avi.png", + "avatar_static": "https://pleroma.example.org/images/avi.png", + "bot": false, + "created_at": "2019-04-23T17:32:04.000Z", + "display_name": "User", + "emojis": [], + "fields": [], + "followers_count": 1, + "following_count": 1, + "header": "https://pleroma.example.org/images/banner.png", + "header_static": "https://pleroma.example.org/images/banner.png", + "id": "9i6dAJqSGSKMzLG2Lo", + "locked": false, + "note": "", + "pleroma": { + "confirmation_pending": false, + "hide_favorites": true, + "hide_followers": false, + "hide_follows": false, + "is_admin": false, + "is_moderator": false, + "relationship": {}, + "tags": [] + }, + "source": { + "note": "", + "pleroma": {}, + "sensitive": false + }, + "statuses_count": 3, + "url": "https://pleroma.example.org/users/user", + "username": "user" + }, + "actor": { + "acct": "lain", + "avatar": "https://pleroma.example.org/images/avi.png", + "avatar_static": "https://pleroma.example.org/images/avi.png", + "bot": false, + "created_at": "2019-03-28T17:36:03.000Z", + "display_name": "Roger Braun", + "emojis": [], + "fields": [], + "followers_count": 1, + "following_count": 1, + "header": "https://pleroma.example.org/images/banner.png", + "header_static": "https://pleroma.example.org/images/banner.png", + "id": "9hEkA5JsvAdlSrocam", + "locked": false, + "note": "", + "pleroma": { + "confirmation_pending": false, + "hide_favorites": false, + "hide_followers": false, + "hide_follows": false, + "is_admin": false, + "is_moderator": false, + "relationship": {}, + "tags": [] + }, + "source": { + "note": "", + "pleroma": {}, + "sensitive": false + }, + "statuses_count": 1, + "url": "https://pleroma.example.org/users/lain", + "username": "lain" + }, + "content": "Please delete it", + "created_at": "2019-04-29T19:48:15.000Z", + "id": "9iJGOv1j8hxuw19bcm", + "state": "open", + "statuses": [ + { + "account": { ... }, + "application": { + "name": "Web", + "website": null + }, + "bookmarked": false, + "card": null, + "content": "@lain click on my link https://www.google.com/", + "created_at": "2019-04-23T19:15:47.000Z", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "9i6mQ9uVrrOmOime8m", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "lain", + "id": "9hEkA5JsvAdlSrocam", + "url": "https://pleroma.example.org/users/lain", + "username": "lain" + }, + { + "acct": "user", + "id": "9i6dAJqSGSKMzLG2Lo", + "url": "https://pleroma.example.org/users/user", + "username": "user" + } + ], + "muted": false, + "pinned": false, + "pleroma": { + "content": { + "text/plain": "@lain click on my link https://www.google.com/" + }, + "conversation_id": 28, + "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/8717b90f-8e09-4b58-97b0-e3305472b396", + "url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m", + "visibility": "direct" + } + ] + } + ] +} +``` + +## `/api/pleroma/admin/reports/:id` +### Get an individual report +- Method `GET` +- Params: + - `id` +- Response: + - On failure: + - 403 Forbidden `{"error": "error_msg"}` + - 404 Not Found `"Not found"` + - On success: JSON, Report object (see above) + +## `/api/pleroma/admin/reports/:id` +### Change the state of the report +- Method `PUT` +- Params: + - `id` + - `state`: required, the new state. Valid values are `open`, `closed` and `resolved` +- Response: + - On failure: + - 400 Bad Request `"Unsupported state"` + - 403 Forbidden `{"error": "error_msg"}` + - 404 Not Found `"Not found"` + - On success: JSON, Report object (see above) + +## `/api/pleroma/admin/reports/:id/respond` +### Respond to a report +- Method `POST` +- Params: + - `id` + - `status`: 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 + +```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" +} +``` + +## `/api/pleroma/admin/statuses/:id` +### Change the scope of an individual reported status +- Method `PUT` +- Params: + - `id` + - `sensitive`: optional, valid values are `true` or `false` + - `visibility`: optional, valid values are `public`, `private` and `unlisted` +- Response: + - On failure: + - 400 Bad Request `"Unsupported visibility"` + - 403 Forbidden `{"error": "error_msg"}` + - 404 Not Found `"Not found"` + - On success: JSON, Mastodon Status entity + +## `/api/pleroma/admin/statuses/:id` +### Delete an individual reported status +- Method `DELETE` +- Params: + - `id` +- Response: + - On failure: + - 403 Forbidden `{"error": "error_msg"}` + - 404 Not Found `"Not found"` + - On success: 200 OK `{}` diff --git a/docs/config.md b/docs/config.md index 470f71b7c..c2af5c012 100644 --- a/docs/config.md +++ b/docs/config.md @@ -286,7 +286,8 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start * ``sts``: Whether to additionally send a `Strict-Transport-Security` header * ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent * ``ct_max_age``: The maximum age for the `Expect-CT` header if sent -* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`. +* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"` +* ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header. ## :mrf_user_allowlist diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 9613a329b..9c0ef92d4 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -12,6 +12,7 @@ This guide will assume you are on Debian Stretch. This guide should also work wi * `erlang-tools` * `erlang-parsetools` * `erlang-eldap`, if you want to enable ldap authenticator +* `erlang-ssh` * `erlang-xmerl` * `git` * `build-essential` @@ -49,7 +50,7 @@ sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb ```shell sudo apt update -sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools +sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh ``` ### Install PleromaBE diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index ac5dcaaee..41cce6792 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -14,6 +14,7 @@ - erlang-dev - erlang-tools - erlang-parsetools +- erlang-ssh - erlang-xmerl (Jessieではバックポートからインストールすること!) - git - build-essential @@ -44,7 +45,7 @@ wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb * ElixirとErlangをインストールします、 ``` -apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools +apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh ``` ### Pleroma BE (バックエンド) をインストールします diff --git a/docs/introduction.md b/docs/introduction.md index 4af0747fe..045dc7c05 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,30 +1,30 @@ # Introduction to Pleroma ## What is Pleroma? -Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3. -It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. -It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. +Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3. +It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. +It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. One account on a instance is enough to talk to the entire fediverse! - + ## How can I use it? -Pleroma instances are already widely deployed, a list can be found here: +Pleroma instances are already widely deployed, a list can be found here: http://distsn.org/pleroma-instances.html -If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too! -Installation instructions can be found here: +If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too! +Installation instructions can be found here: [main Pleroma wiki](/) - + ## I got an account, now what? -Great! Now you can explore the fediverse! -- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password. -(If you don't have one yet, click on Register) :slightly_smiling_face: +Great! Now you can explore the fediverse! +- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password. +(If you don't have one yet, click on Register) :slightly_smiling_face: At this point you will have two columns in front of you. ### Left column -- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers). -Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000). -If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile: +- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers). +Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000). +If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile: To post your status, simply press Submit. - second block: Here you can switch between the different timelines: @@ -38,7 +38,7 @@ To post your status, simply press Submit. - fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses. ### Right column -This is where the interesting stuff happens! :slight_smile: +This is where the interesting stuff happens! :slight_smile: Depending on the timeline you will see different statuses, but each status has a standard structure: - Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status). - A + button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime! @@ -47,9 +47,9 @@ Depending on the timeline you will see different statuses, but each status has a - Four buttons (left to right): Reply, Repeat, Favorite, Delete. ## Mastodon interface -If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile: -Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks: -For more information on the Mastodon interface, please look here: +If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile: +Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks: +For more information on the Mastodon interface, please look here: https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index ab9a3a7ff..f650b447d 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -4,6 +4,9 @@ defmodule Mix.Tasks.Pleroma.Database do alias Mix.Tasks.Pleroma.Common + alias Pleroma.Conversation + alias Pleroma.Repo + alias Pleroma.User require Logger use Mix.Task @@ -19,6 +22,14 @@ defmodule Mix.Tasks.Pleroma.Database do Options: - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references + + ## Create a conversation for all existing DMs. Can be safely re-run. + + mix pleroma.database bump_all_conversations + + ## Remove duplicated items from following and update followers count for all users + + mix pleroma.database update_users_following_followers_counts """ def run(["remove_embedded_objects" | args]) do {options, [], []} = @@ -32,7 +43,7 @@ def run(["remove_embedded_objects" | args]) do Common.start_pleroma() Logger.info("Removing embedded objects") - Pleroma.Repo.query!( + Repo.query!( "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;", [], timeout: :infinity @@ -41,11 +52,24 @@ def run(["remove_embedded_objects" | args]) do if Keyword.get(options, :vacuum) do Logger.info("Runnning VACUUM FULL") - Pleroma.Repo.query!( + Repo.query!( "vacuum full;", [], timeout: :infinity ) end end + + def run(["bump_all_conversations"]) do + Common.start_pleroma() + Conversation.bump_for_all_activities() + end + + def run(["update_users_following_followers_counts"]) do + Common.start_pleroma() + + users = Repo.all(User) + Enum.each(users, &User.remove_duplicated_following/1) + Enum.each(users, &User.update_follower_count/1) + end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index d130ff8c9..25fc40ea7 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -77,6 +77,10 @@ defmodule Mix.Tasks.Pleroma.User do ## Delete tags from a user. mix pleroma.user untag NICKNAME TAGS + + ## Toggle confirmation of the user's account. + + mix pleroma.user toggle_confirmed NICKNAME """ def run(["new", nickname, email | rest]) do {options, [], []} = @@ -388,6 +392,21 @@ def run(["delete_activities", nickname]) do end end + def run(["toggle_confirmed", nickname]) do + Common.start_pleroma() + + with %User{} = user <- User.get_cached_by_nickname(nickname) do + {:ok, user} = User.toggle_confirmation(user) + + message = if user.info.confirmation_pending, do: "needs", else: "doesn't need" + + Mix.shell().info("#{nickname} #{message} confirmation.") + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + defp set_moderator(user, value) do info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 4a0919478..4e54b15ba 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -60,21 +60,24 @@ defmodule Pleroma.Activity do timestamps() end - def with_preloaded_object(query) do - query - |> join( - :inner, - [activity], - o in Object, + def with_joined_object(query) do + join(query, :inner, [activity], o in Object, on: fragment( "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", o.data, activity.data, activity.data - ) + ), + as: :object ) - |> preload([activity, object], object: object) + end + + def with_preloaded_object(query) do + query + |> has_named_binding?(:object) + |> if(do: query, else: with_joined_object(query)) + |> preload([activity, object: object], object: object) end def with_preloaded_bookmark(query, %User{} = user) do @@ -108,7 +111,7 @@ def get_bookmark(_, _), do: nil def change(struct, params \\ %{}) do struct - |> cast(params, [:data]) + |> cast(params, [:data, :recipients]) |> validate_required([:data]) |> unique_constraint(:ap_id, name: :activities_unique_apid_index) end diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index 106fe5d18..f34be961f 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -95,7 +95,6 @@ def handle_command(state, "home") do activities = [user.ap_id | user.following] |> ActivityPub.fetch_activities(params) - |> ActivityPub.contain_timeline(user) Enum.each(activities, fn activity -> puts_activity(activity) diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 0db195988..238c1acf2 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -45,7 +45,7 @@ def get_for_ap_id(ap_id) do 2. Create a participation for all the people involved who don't have one already 3. Bump all relevant participations to 'unread' """ - def create_or_bump_for(activity) do + def create_or_bump_for(activity, opts \\ []) do with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), "Create" <- activity.data["type"], object <- Pleroma.Object.normalize(activity), @@ -58,7 +58,7 @@ def create_or_bump_for(activity) do participations = Enum.map(users, fn user -> {:ok, participation} = - Participation.create_for_user_and_conversation(user, conversation) + Participation.create_for_user_and_conversation(user, conversation, opts) participation end) @@ -72,4 +72,21 @@ def create_or_bump_for(activity) do e -> {:error, e} end end + + @doc """ + This is only meant to be run by a mix task. It creates conversations/participations for all direct messages in the database. + """ + def bump_for_all_activities do + stream = + Pleroma.Web.ActivityPub.ActivityPub.fetch_direct_messages_query() + |> Repo.stream() + + Repo.transaction( + fn -> + stream + |> Enum.each(fn a -> create_or_bump_for(a, read: true) end) + end, + timeout: :infinity + ) + end end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 61021fb18..2a11f9069 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -22,15 +22,17 @@ defmodule Pleroma.Conversation.Participation do def creation_cng(struct, params) do struct - |> cast(params, [:user_id, :conversation_id]) + |> cast(params, [:user_id, :conversation_id, :read]) |> validate_required([:user_id, :conversation_id]) end - def create_for_user_and_conversation(user, conversation) do + def create_for_user_and_conversation(user, conversation, opts \\ []) do + read = !!opts[:read] + %__MODULE__{} - |> creation_cng(%{user_id: user.id, conversation_id: conversation.id}) + |> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read}) |> Repo.insert( - on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]], + on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]], returning: true, conflict_target: [:user_id, :conversation_id] ) diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index df0f72f96..d0e254362 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -29,7 +29,7 @@ def report(to, reporter, account, statuses, comment) do end statuses_html = - if length(statuses) > 0 do + if is_list(statuses) && length(statuses) > 0 do statuses_list_html = statuses |> Enum.map(fn diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 79efc29f0..90457dadf 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -38,7 +38,8 @@ def get_filters(%User{id: user_id} = _user) do query = from( f in Pleroma.Filter, - where: f.user_id == ^user_id + where: f.user_id == ^user_id, + order_by: [desc: :id] ) Repo.all(query) diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex new file mode 100644 index 000000000..317fd5445 --- /dev/null +++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do + import Plug.Conn + alias Pleroma.Config + alias Pleroma.User + + def init(options) do + options + end + + def call(conn, _) do + public? = Config.get!([:instance, :public]) + + case {public?, conn} do + {true, _} -> + conn + + {false, %{assigns: %{user: %User{}}}} -> + conn + + {false, _} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."})) + |> halt + end + end +end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index a476f1d49..485ddfbc7 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -20,8 +20,9 @@ def call(conn, _options) do defp headers do referrer_policy = Config.get([:http_security, :referrer_policy]) + report_uri = Config.get([:http_security, :report_uri]) - [ + headers = [ {"x-xss-protection", "1; mode=block"}, {"x-permitted-cross-domain-policies", "none"}, {"x-frame-options", "DENY"}, @@ -30,12 +31,27 @@ defp headers do {"x-download-options", "noopen"}, {"content-security-policy", csp_string() <> ";"} ] + + if report_uri do + report_group = %{ + "group" => "csp-endpoint", + "max-age" => 10_886_400, + "endpoints" => [ + %{"url" => report_uri} + ] + } + + headers ++ [{"reply-to", Jason.encode!(report_group)}] + else + headers + end end defp csp_string do scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] static_url = Pleroma.Web.Endpoint.static_url() websocket_url = Pleroma.Web.Endpoint.websocket_url() + report_uri = Config.get([:http_security, :report_uri]) connect_src = "connect-src 'self' #{static_url} #{websocket_url}" @@ -53,7 +69,7 @@ defp csp_string do "script-src 'self'" end - [ + main_part = [ "default-src 'none'", "base-uri 'self'", "frame-ancestors 'none'", @@ -63,11 +79,14 @@ defp csp_string do "font-src 'self'", "manifest-src 'self'", connect_src, - script_src, - if scheme == "https" do - "upgrade-insecure-requests" - end + script_src ] + + report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: [] + + insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: [] + + (main_part ++ report ++ insecure) |> Enum.join("; ") end diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex index 21c195713..e2874c469 100644 --- a/lib/pleroma/plugs/http_signature.ex +++ b/lib/pleroma/plugs/http_signature.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.HTTPSignatures import Plug.Conn require Logger diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex new file mode 100644 index 000000000..b7ecf00a0 --- /dev/null +++ b/lib/pleroma/signature.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Signature do + @behaviour HTTPSignatures.Adapter + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Salmon + alias Pleroma.Web.WebFinger + + def fetch_public_key(conn) do + with actor_id <- Utils.get_ap_id(conn.params["actor"]), + {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do + {:ok, public_key} + else + e -> + {:error, e} + end + end + + def refetch_public_key(conn) do + with actor_id <- Utils.get_ap_id(conn.params["actor"]), + {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), + {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do + {:ok, public_key} + else + e -> + {:error, e} + end + end + + def sign(%User{} = user, headers) do + with {:ok, %{info: %{keys: keys}}} <- WebFinger.ensure_keys_present(user), + {:ok, private_key, _} <- Salmon.keys_from_pem(keys) do + HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) + end + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c6a562a61..9607c1f03 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -55,7 +55,7 @@ defmodule Pleroma.User do field(:last_refreshed_at, :naive_datetime_usec) has_many(:notifications, Notification) has_many(:registrations, Registration) - embeds_one(:info, Pleroma.User.Info) + embeds_one(:info, User.Info) timestamps() end @@ -166,7 +166,7 @@ def remote_user_creation(params) do def update_changeset(struct, params \\ %{}) do struct - |> cast(params, [:bio, :name, :avatar]) + |> cast(params, [:bio, :name, :avatar, :following]) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: 5000) @@ -233,7 +233,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> validate_confirmation(:password) |> unique_constraint(:email) |> unique_constraint(:nickname) - |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) + |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames])) |> validate_format(:nickname, local_nickname_regex()) |> validate_format(:email, @email_regex) |> validate_length(:bio, max: 1000) @@ -278,7 +278,7 @@ def register(%Ecto.Changeset{} = changeset) do with {:ok, user} <- Repo.insert(changeset), {:ok, user} <- autofollow_users(user), {:ok, user} <- set_cache(user), - {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user), + {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- try_send_confirmation_email(user) do {:ok, user} end @@ -709,6 +709,18 @@ def update_follower_count(%User{} = user) do end end + def remove_duplicated_following(%User{following: following} = user) do + uniq_following = Enum.uniq(following) + + if length(following) == length(uniq_following) do + {:ok, user} + else + user + |> update_changeset(%{following: uniq_following}) + |> update_and_set_cache() + end + end + @spec get_users_from_set([String.t()], boolean()) :: [User.t()] def get_users_from_set(ap_ids, local_only \\ true) do criteria = %{ap_id: ap_ids, deactivated: false} @@ -1378,4 +1390,17 @@ def all_superusers do def showing_reblogs?(%User{} = user, %User{} = target) do target.ap_id not in user.info.muted_reblogs end + + @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()} + def toggle_confirmation(%User{} = user) do + need_confirmation? = !user.info.confirmation_pending + + info_changeset = + User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?) + + user + |> change() + |> put_embed(:info, info_changeset) + |> update_and_set_cache() + end end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 5a50ee639..5f0cefc00 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -212,7 +212,7 @@ def profile_update(info, params) do ]) end - @spec confirmation_changeset(Info.t(), keyword()) :: Ecto.Changerset.t() + @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t() def confirmation_changeset(info, opts) do need_confirmation? = Keyword.get(opts, :need_confirmation) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 233fee4fa..5c3156978 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -540,8 +540,6 @@ defp restrict_visibility(query, %{visibility: visibility}) ) ) - Ecto.Adapters.SQL.to_sql(:all, Repo, query) - query else Logger.error("Could not restrict visibility to #{visibility}") @@ -557,8 +555,6 @@ defp restrict_visibility(query, %{visibility: visibility}) fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) ) - Ecto.Adapters.SQL.to_sql(:all, Repo, query) - query end @@ -569,6 +565,18 @@ defp restrict_visibility(_query, %{visibility: visibility}) defp restrict_visibility(query, _visibility), do: query + defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do + query = + from( + a in query, + where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) + ) + + query + end + + defp restrict_thread_visibility(query, _), do: query + def fetch_user_activities(user, reading_user, params \\ %{}) do params = params @@ -695,6 +703,12 @@ defp restrict_type(query, %{"type" => type}) do defp restrict_type(query, _), do: query + defp restrict_state(query, %{"state" => state}) do + from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state)) + end + + defp restrict_state(query, _), do: query + defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do from( activity in query, @@ -750,8 +764,11 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do blocks = info.blocks || [] domain_blocks = info.domain_blocks || [] + query = + if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) + from( - activity in query, + [activity, object: o] in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocks), where: fragment("not (? && ?)", activity.recipients, ^blocks), where: @@ -761,7 +778,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do activity.data, ^blocks ), - where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks) + where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks), + where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks) ) end @@ -843,11 +861,13 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_local(opts) |> restrict_actor(opts) |> restrict_type(opts) + |> restrict_state(opts) |> restrict_favorited_by(opts) |> restrict_blocked(opts) |> restrict_muted(opts) |> restrict_media(opts) |> restrict_visibility(opts) + |> restrict_thread_visibility(opts) |> restrict_replies(opts) |> restrict_reblogs(opts) |> restrict_pinned(opts) @@ -966,11 +986,10 @@ def contain_activity(%Activity{} = activity, %User{} = user) do contain_broken_threads(activity, user) end - # do post-processing on a timeline - def contain_timeline(timeline, user) do - timeline - |> Enum.filter(fn activity -> - contain_activity(activity, user) - end) + def fetch_direct_messages_query do + Activity + |> restrict_type(%{"type" => "Create"}) + |> restrict_visibility(%{visibility: "direct"}) + |> order_by([activity], asc: activity.id) end end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 2f105700b..50426e920 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -55,7 +55,7 @@ defp check_media_nsfw( object = if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do tags = (child_object["tag"] || []) ++ ["nsfw"] - child_object = Map.put(child_object, "tags", tags) + child_object = Map.put(child_object, "tag", tags) child_object = Map.put(child_object, "sensitive", true) Map.put(object, "object", child_object) else diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index b52be30e7..6683b8d8e 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -31,7 +31,7 @@ defp process_tag( object = object - |> Map.put("tags", tags) + |> Map.put("tag", tags) |> Map.put("sensitive", true) message = Map.put(message, "object", object) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 8e3af0a81..11dba87de 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -54,7 +54,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") signature = - Pleroma.Web.HTTPSignatures.sign(actor, %{ + Pleroma.Signature.sign(actor, %{ host: host, "content-length": byte_size(json), digest: digest, diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 508f3532f..5edd8ccc7 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Object.Containment alias Pleroma.Repo alias Pleroma.User - alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 236d1b4ac..ca8a0844b 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -20,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Logger @supported_object_types ["Article", "Note", "Video", "Page"] + @supported_report_states ~w(open closed resolved) + @valid_visibilities ~w(public unlisted private direct) # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. @@ -670,7 +672,8 @@ def make_flag_data(params, additional) do "actor" => params.actor.ap_id, "content" => params.content, "object" => object, - "context" => params.context + "context" => params.context, + "state" => "open" } |> Map.merge(additional) end @@ -713,4 +716,77 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do end end end + + #### Report-related helpers + + def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do + with new_data <- Map.put(activity.data, "state", state), + changeset <- Changeset.change(activity, data: new_data), + {:ok, activity} <- Repo.update(changeset) do + {:ok, activity} + end + end + + def update_report_state(_, _), do: {:error, "Unsupported state"} + + def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do + [to, cc, recipients] = + activity + |> get_updated_targets(visibility) + |> Enum.map(&Enum.uniq/1) + + object_data = + activity.object.data + |> Map.put("to", to) + |> Map.put("cc", cc) + + {:ok, object} = + activity.object + |> Object.change(%{data: object_data}) + |> Object.update_and_set_cache() + + activity_data = + activity.data + |> Map.put("to", to) + |> Map.put("cc", cc) + + activity + |> Map.put(:object, object) + |> Activity.change(%{data: activity_data, recipients: recipients}) + |> Repo.update() + end + + def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"} + + defp get_updated_targets( + %Activity{data: %{"to" => to} = data, recipients: recipients}, + visibility + ) do + cc = Map.get(data, "cc", []) + follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address + public = "https://www.w3.org/ns/activitystreams#Public" + + case visibility do + "public" -> + to = [public | List.delete(to, follower_address)] + cc = [follower_address | List.delete(cc, public)] + recipients = [public | recipients] + [to, cc, recipients] + + "private" -> + to = [follower_address | List.delete(to, public)] + cc = List.delete(cc, public) + recipients = List.delete(recipients, public) + [to, cc, recipients] + + "unlisted" -> + to = [follower_address | List.delete(to, public)] + cc = [public | List.delete(cc, follower_address)] + recipients = recipients ++ [follower_address, public] + [to, cc, recipients] + + _ -> + [to, cc, recipients] + end + end end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 6dee61dd6..93b50ee47 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -1,6 +1,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Repo alias Pleroma.User def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false @@ -13,11 +14,12 @@ def is_public?(data) do end def is_private?(activity) do - unless is_public?(activity) do - follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address - Enum.any?(activity.data["to"], &(&1 == follower_address)) + with false <- is_public?(activity), + %User{follower_address: follower_address} <- + User.get_cached_by_ap_id(activity.data["actor"]) do + follower_address in activity.data["to"] else - false + _ -> false end end @@ -38,24 +40,37 @@ def visible_for_user?(activity, user) do visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) end - # guard - def entire_thread_visible_for_user?(nil, _user), do: false + def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do + {:ok, %{rows: [[result]]}} = + Ecto.Adapters.SQL.query(Repo, "SELECT thread_visibility($1, $2)", [ + user.ap_id, + activity.data["id"] + ]) - # XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop - # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + result + end - def entire_thread_visible_for_user?( - %Activity{} = tail, - # %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, - user - ) do - case Object.normalize(tail) do - %{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) -> - parent = Activity.get_in_reply_to_activity(tail) - visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) + def get_visibility(object) do + public = "https://www.w3.org/ns/activitystreams#Public" + to = object.data["to"] || [] + cc = object.data["cc"] || [] - _ -> - visible_for_user?(tail, user) + cond do + public in to -> + "public" + + public in cc -> + "unlisted" + + # this should use the sql for the object's activity + Enum.any?(to, &String.contains?(&1, "/followers")) -> + "private" + + length(cc) > 0 -> + "private" + + true -> + "direct" end end end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index e00b33aba..de2a13c01 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -4,11 +4,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller + alias Pleroma.Activity alias Pleroma.User alias Pleroma.UserInviteToken + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView import Pleroma.Web.ControllerHelper, only: [json_response: 3] @@ -287,12 +292,88 @@ def get_password_reset(conn, %{"nickname" => nickname}) do |> json(token.token) end + def list_reports(conn, params) do + params = + params + |> Map.put("type", "Flag") + |> Map.put("skip_preload", true) + + reports = + [] + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() + + conn + |> put_view(ReportView) + |> render("index.json", %{reports: reports}) + end + + def report_show(conn, %{"id" => id}) do + with %Activity{} = report <- Activity.get_by_id(id) do + conn + |> put_view(ReportView) + |> render("show.json", %{report: report}) + else + _ -> {:error, :not_found} + end + end + + def report_update_state(conn, %{"id" => id, "state" => state}) do + with {:ok, report} <- CommonAPI.update_report_state(id, state) do + conn + |> put_view(ReportView) + |> render("show.json", %{report: report}) + 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) + + conn + |> put_view(StatusView) + |> render("status.json", %{activity: activity}) + else + true -> + {:param_cast, nil} + + nil -> + {:error, :not_found} + end + end + + def status_update(conn, %{"id" => id} = params) do + with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do + conn + |> put_view(StatusView) + |> render("status.json", %{activity: activity}) + end + end + + def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + json(conn, %{}) + end + end + def errors(conn, {:error, :not_found}) do conn |> put_status(404) |> json("Not found") end + def errors(conn, {:error, reason}) do + conn + |> put_status(400) + |> json(reason) + end + def errors(conn, {:param_cast, _}) do conn |> put_status(400) diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex new file mode 100644 index 000000000..47a73dc7e --- /dev/null +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ReportView do + use Pleroma.Web, :view + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("index.json", %{reports: reports}) do + %{ + reports: render_many(reports, __MODULE__, "show.json", as: :report) + } + end + + def render("show.json", %{report: report}) do + user = User.get_cached_by_ap_id(report.data["actor"]) + created_at = Utils.to_masto_date(report.data["published"]) + + [account_ap_id | status_ap_ids] = report.data["object"] + account = User.get_cached_by_ap_id(account_ap_id) + + statuses = + Enum.map(status_ap_ids, fn ap_id -> + Activity.get_by_ap_id_with_object(ap_id) + end) + + %{ + id: report.id, + account: AccountView.render("account.json", %{user: account}), + actor: AccountView.render("account.json", %{user: user}), + content: report.data["content"], + created_at: created_at, + statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), + state: report.data["state"] + } + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index b53869c75..a599ffee5 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -71,6 +71,9 @@ def delete(activity_id, user) do {:ok, _} <- unpin(activity_id, user), {:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} + else + _ -> + {:error, "Could not delete"} end end @@ -116,32 +119,34 @@ def unfavorite(id_or_ap_id, user) do end end - def get_visibility(%{"visibility" => visibility}) + def get_visibility(%{"visibility" => visibility}, in_reply_to) when visibility in ~w{public unlisted private direct}, - do: visibility + do: {visibility, get_replied_to_visibility(in_reply_to)} - def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(status_id) do - case get_replied_to_activity(status_id) do - nil -> - "public" + def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do + visibility = get_replied_to_visibility(in_reply_to) + {visibility, visibility} + end - in_reply_to -> - # XXX: these heuristics should be moved out of MastodonAPI. - with %Object{} = object <- Object.normalize(in_reply_to) do - Pleroma.Web.MastodonAPI.StatusView.get_visibility(object) - end + def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)} + + def get_replied_to_visibility(nil), do: nil + + def get_replied_to_visibility(activity) do + with %Object{} = object <- Object.normalize(activity) do + Pleroma.Web.ActivityPub.Visibility.get_visibility(object) end end - def get_visibility(_), do: "public" - def post(user, %{"status" => status} = data) do - visibility = get_visibility(data) limit = Pleroma.Config.get([:instance, :limit]) with status <- String.trim(status), attachments <- attachments_from_ids(data), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), + {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to), + {_, false} <- + {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"}, {content_html, mentions, tags} <- make_content_html( status, @@ -185,6 +190,8 @@ def post(user, %{"status" => status} = data) do ) res + else + e -> {:error, e} end end @@ -193,7 +200,7 @@ def update(user) do user = with emoji <- emoji_from_profile(user), source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji), - info_cng <- Pleroma.User.Info.set_source_data(user.info, source_data), + info_cng <- User.Info.set_source_data(user.info, source_data), change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), {:ok, user} <- User.update_and_set_cache(change) do user @@ -226,7 +233,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do } = activity <- get_by_id_or_ap_id(id_or_ap_id), true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"), %{valid?: true} = info_changeset <- - Pleroma.User.Info.add_pinnned_activity(user.info, activity), + User.Info.add_pinnned_activity(user.info, activity), changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), {:ok, _user} <- User.update_and_set_cache(changeset) do @@ -243,7 +250,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do def unpin(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), %{valid?: true} = info_changeset <- - Pleroma.User.Info.remove_pinnned_activity(user.info, activity), + User.Info.remove_pinnned_activity(user.info, activity), changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), {:ok, _user} <- User.update_and_set_cache(changeset) do @@ -311,6 +318,60 @@ def report(user, data) do end end + def update_report_state(activity_id, state) do + with %Activity{} = activity <- Activity.get_by_id(activity_id), + {:ok, activity} <- Utils.update_report_state(activity, state) do + {:ok, activity} + else + nil -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} + + _ -> + {:error, "Could not update state"} + end + end + + def update_activity_scope(activity_id, opts \\ %{}) do + with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), + {:ok, activity} <- toggle_sensitive(activity, opts), + {:ok, activity} <- set_visibility(activity, opts) do + {:ok, activity} + else + nil -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} + end + end + + defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do + toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)}) + end + + defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive}) + when is_boolean(sensitive) do + new_data = Map.put(object.data, "sensitive", sensitive) + + {:ok, object} = + object + |> Object.change(%{data: new_data}) + |> Object.update_and_set_cache() + + {:ok, Map.put(activity, :object, object)} + end + + defp toggle_sensitive(activity, _), do: {:ok, activity} + + defp set_visibility(activity, %{"visibility" => visibility}) do + Utils.update_activity_visibility(activity, visibility) + end + + defp set_visibility(activity, _), do: {:ok, activity} + def hide_reblogs(user, muted) do ap_id = muted.ap_id diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 1dfe50b40..bee2fd159 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -237,13 +237,11 @@ def make_note_data( "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() } - if in_reply_to do - in_reply_to_object = Object.normalize(in_reply_to) - - object - |> Map.put("inReplyTo", in_reply_to_object.data["id"]) + with false <- is_nil(in_reply_to), + %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do + Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) else - object + _ -> object end end diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex index 916bcdcba..70f870244 100644 --- a/lib/pleroma/web/federator/publisher.ex +++ b/lib/pleroma/web/federator/publisher.ex @@ -31,7 +31,7 @@ defmodule Pleroma.Web.Federator.Publisher do """ @spec enqueue_one(module(), Map.t()) :: :ok def enqueue_one(module, %{} = params), - do: PleromaJobQueue.enqueue(:federation_outgoing, __MODULE__, [:publish_one, module, params]) + do: PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_one, module, params]) @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} def perform(:publish_one, module, params) do @@ -52,9 +52,9 @@ def perform(type, _, _) do @doc """ Relays an activity to all specified peers. """ - @callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()} + @callback publish(User.t(), Activity.t()) :: :ok | {:error, any()} - @spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok + @spec publish(User.t(), Activity.t()) :: :ok def publish(%User{} = user, %Activity{} = activity) do Config.get([:instance, :federation_publisher_modules]) |> Enum.each(fn module -> @@ -70,9 +70,9 @@ def publish(%User{} = user, %Activity{} = activity) do @doc """ Gathers links used by an outgoing federation module for WebFinger output. """ - @callback gather_webfinger_links(Pleroma.User.t()) :: list() + @callback gather_webfinger_links(User.t()) :: list() - @spec gather_webfinger_links(Pleroma.User.t()) :: list() + @spec gather_webfinger_links(User.t()) :: list() def gather_webfinger_links(%User{} = user) do Config.get([:instance, :federation_publisher_modules]) |> Enum.reduce([], fn module, links -> diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex deleted file mode 100644 index 8e2e2a44b..000000000 --- a/lib/pleroma/web/http_signatures/http_signatures.ex +++ /dev/null @@ -1,91 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -# https://tools.ietf.org/html/draft-cavage-http-signatures-08 -defmodule Pleroma.Web.HTTPSignatures do - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Utils - - require Logger - - def split_signature(sig) do - default = %{"headers" => "date"} - - sig = - sig - |> String.trim() - |> String.split(",") - |> Enum.reduce(default, fn part, acc -> - [key | rest] = String.split(part, "=") - value = Enum.join(rest, "=") - Map.put(acc, key, String.trim(value, "\"")) - end) - - Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/)) - end - - def validate(headers, signature, public_key) do - sigstring = build_signing_string(headers, signature["headers"]) - Logger.debug("Signature: #{signature["signature"]}") - Logger.debug("Sigstring: #{sigstring}") - {:ok, sig} = Base.decode64(signature["signature"]) - :public_key.verify(sigstring, :sha256, sig, public_key) - end - - def validate_conn(conn) do - # TODO: How to get the right key and see if it is actually valid for that request. - # For now, fetch the key for the actor. - with actor_id <- Utils.get_ap_id(conn.params["actor"]), - {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do - if validate_conn(conn, public_key) do - true - else - Logger.debug("Could not validate, re-fetching user and trying one more time") - # Fetch user anew and try one more time - with actor_id <- Utils.get_ap_id(conn.params["actor"]), - {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), - {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do - validate_conn(conn, public_key) - end - end - else - _e -> - Logger.debug("Could not public key!") - false - end - end - - def validate_conn(conn, public_key) do - headers = Enum.into(conn.req_headers, %{}) - signature = split_signature(headers["signature"]) - validate(headers, signature, public_key) - end - - def build_signing_string(headers, used_headers) do - used_headers - |> Enum.map(fn header -> "#{header}: #{headers[header]}" end) - |> Enum.join("\n") - end - - def sign(user, headers) do - with {:ok, %{info: %{keys: keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user), - {:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do - sigstring = build_signing_string(headers, Map.keys(headers)) - - signature = - :public_key.sign(sigstring, :sha256, private_key) - |> Base.encode64() - - [ - keyId: user.ap_id <> "#main-key", - algorithm: "rsa-sha256", - headers: Map.keys(headers) |> Enum.join(" "), - signature: signature - ] - |> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end) - |> Enum.join(",") - end - end -end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 87e597074..1b776fbca 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -303,7 +303,6 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do activities = [user.ap_id | user.following] |> ActivityPub.fetch_activities(params) - |> ActivityPub.contain_timeline(user) |> Enum.reverse() conn @@ -1223,7 +1222,7 @@ def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_id accounts |> Enum.each(fn account_id -> with %Pleroma.List{} = list <- Pleroma.List.get(id, user), - %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do + %User{} = followed <- User.get_cached_by_id(account_id) do Pleroma.List.unfollow(list, followed) end end) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index bd2372944..c93d915e5 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -16,6 +16,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy + import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] + # TODO: Add cached version. defp get_replied_to_activities(activities) do activities @@ -340,30 +342,6 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do end end - def get_visibility(object) do - public = "https://www.w3.org/ns/activitystreams#Public" - to = object.data["to"] || [] - cc = object.data["cc"] || [] - - cond do - public in to -> - "public" - - public in cc -> - "unlisted" - - # this should use the sql for the object's activity - Enum.any?(to, &String.contains?(&1, "/followers")) -> - "private" - - length(cc) > 0 -> - "private" - - true -> - "direct" - end - end - def render_content(%{data: %{"type" => "Video"}} = object) do with name when not is_nil(name) and name != "" <- object.data["name"] do "

#{name}

#{object.data["content"]}" diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index b47688de1..18973413e 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.OAuth.Authorization do field(:scopes, {:array, :string}, default: []) field(:valid_until, :naive_datetime_usec) field(:used, :boolean, default: false) - belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) + belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:app, App) timestamps() diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index ef047d565..66c95c2e9 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Web.OAuth.Token do field(:refresh_token, :string) field(:scopes, {:array, :string}, default: []) field(:valid_until, :naive_datetime_usec) - belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) + belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:app, App) timestamps() diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 80af0afe1..6a4e4a1d4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -84,11 +84,13 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.EnsureUserKeyPlug) end - pipeline :oauth_read_or_unauthenticated do + pipeline :oauth_read_or_public do plug(Pleroma.Plugs.OAuthScopesPlug, %{ scopes: ["read"], fallback: :proceed_unauthenticated }) + + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) end pipeline :oauth_read do @@ -192,6 +194,14 @@ defmodule Pleroma.Web.Router do get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) + + get("/reports", AdminAPIController, :list_reports) + get("/reports/:id", AdminAPIController, :report_show) + put("/reports/:id", AdminAPIController, :report_update_state) + post("/reports/:id/respond", AdminAPIController, :report_respond) + + put("/statuses/:id", AdminAPIController, :status_update) + delete("/statuses/:id", AdminAPIController, :status_delete) end scope "/", Pleroma.Web.TwitterAPI do @@ -404,7 +414,7 @@ defmodule Pleroma.Web.Router do get("/accounts/search", MastodonAPIController, :account_search) scope [] do - pipe_through(:oauth_read_or_unauthenticated) + pipe_through(:oauth_read_or_public) get("/timelines/public", MastodonAPIController, :public_timeline) get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) @@ -425,7 +435,7 @@ defmodule Pleroma.Web.Router do end scope "/api/v2", Pleroma.Web.MastodonAPI do - pipe_through([:api, :oauth_read_or_unauthenticated]) + pipe_through([:api, :oauth_read_or_public]) get("/search", MastodonAPIController, :search2) end @@ -455,7 +465,7 @@ defmodule Pleroma.Web.Router do ) scope [] do - pipe_through(:oauth_read_or_unauthenticated) + pipe_through(:oauth_read_or_public) get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline) get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline) @@ -473,7 +483,7 @@ defmodule Pleroma.Web.Router do end scope "/api", Pleroma.Web do - pipe_through([:api, :oauth_read_or_unauthenticated]) + pipe_through([:api, :oauth_read_or_public]) get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline) @@ -487,7 +497,7 @@ defmodule Pleroma.Web.Router do end scope "/api", Pleroma.Web, as: :twitter_api_search do - pipe_through([:api, :oauth_read_or_unauthenticated]) + pipe_through([:api, :oauth_read_or_public]) get("/pleroma/search_user", TwitterAPI.Controller, :search_user) end @@ -671,7 +681,7 @@ defmodule Pleroma.Web.Router do delete("/auth/sign_out", MastodonAPIController, :logout) scope [] do - pipe_through(:oauth_read_or_unauthenticated) + pipe_through(:oauth_read_or_public) get("/web/*path", MastodonAPIController, :index) end end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 3c5a70be9..31e86685a 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -101,9 +101,7 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do |> Map.put("blocking_user", user) |> Map.put("user", user) - activities = - ActivityPub.fetch_activities([user.ap_id | user.following], params) - |> ActivityPub.contain_timeline(user) + activities = ActivityPub.fetch_activities([user.ap_id | user.following], params) conn |> put_view(ActivityView) diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index d084ad734..44bcafe0e 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -310,7 +310,7 @@ def render( "tags" => tags, "activity_type" => "post", "possibly_sensitive" => possibly_sensitive, - "visibility" => StatusView.get_visibility(object), + "visibility" => Pleroma.Web.ActivityPub.Visibility.get_visibility(object), "summary" => summary, "summary_html" => summary |> Formatter.emojify(object.data["emoji"]), "card" => card, diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 3a3b98a10..1239b962a 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -99,7 +99,7 @@ def ensure_keys_present(user) do info_cng = info - |> Pleroma.User.Info.set_keys(pem) + |> User.Info.set_keys(pem) cng = Ecto.Changeset.change(user) diff --git a/mix.exs b/mix.exs index 2888b69e2..95c052c34 100644 --- a/mix.exs +++ b/mix.exs @@ -13,6 +13,7 @@ def project do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), + test_coverage: [tool: ExCoveralls], # Docs name: "Pleroma", @@ -106,6 +107,9 @@ defp deps do {:auto_linker, git: "https://git.pleroma.social/pleroma/auto_linker.git", ref: "c00c4e75b35367fa42c95ffd9b8c455bf9995829"}, + {:http_signatures, + git: "https://git.pleroma.social/pleroma/http_signatures.git", + ref: "9789401987096ead65646b52b5a2ca6bf52fc531"}, {:pleroma_job_queue, "~> 0.2.0"}, {:telemetry, "~> 0.3"}, {:prometheus_ex, "~> 3.0"}, @@ -118,7 +122,8 @@ defp deps do {:benchee, "~> 1.0"}, {:esshd, "~> 0.1.0"}, {:ex_rated, "~> 1.2"}, - {:plug_static_index_html, "~> 1.0.0"} + {:plug_static_index_html, "~> 1.0.0"}, + {:excoveralls, "~> 0.11.1", only: :test} ] ++ oauth_deps end diff --git a/mix.lock b/mix.lock index 714a8a2b1..05eb004e0 100644 --- a/mix.lock +++ b/mix.lock @@ -31,12 +31,14 @@ "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_rated": {:hex, :ex_rated, "1.3.2", "6aeb32abb46ea6076f417a9ce8cb1cf08abf35fb2d42375beaad4dd72b550bf1", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, + "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, + "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "9789401987096ead65646b52b5a2ca6bf52fc531", [ref: "9789401987096ead65646b52b5a2ca6bf52fc531"]}, "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, diff --git a/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs b/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs new file mode 100644 index 000000000..0d3d253b6 --- /dev/null +++ b/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetDefaultStateToReports do + use Ecto.Migration + + def up do + execute """ + UPDATE activities AS a + SET data = jsonb_set(data, '{state}', '"open"', true) + WHERE data->>'type' = 'Flag' + """ + end + + def down do + execute """ + UPDATE activities AS a + SET data = data #- '{state}' + WHERE data->>'type' = 'Flag' + """ + end +end diff --git a/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs new file mode 100644 index 000000000..dc9abc998 --- /dev/null +++ b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs @@ -0,0 +1,73 @@ +defmodule Pleroma.Repo.Migrations.AddThreadVisibilityFunction do + use Ecto.Migration + @disable_ddl_transaction true + + def up do + statement = """ + CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$ + DECLARE + public varchar := 'https://www.w3.org/ns/activitystreams#Public'; + child objects%ROWTYPE; + activity activities%ROWTYPE; + actor_user users%ROWTYPE; + author_fa varchar; + valid_recipients varchar[]; + BEGIN + --- Fetch our actor. + SELECT * INTO actor_user FROM users WHERE users.ap_id = actor; + + --- Fetch our initial activity. + SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + + LOOP + --- Ensure that we have an activity before continuing. + --- If we don't, the thread is not satisfiable. + IF activity IS NULL THEN + RETURN false; + END IF; + + --- We only care about Create activities. + IF activity.data->>'type' != 'Create' THEN + RETURN true; + END IF; + + --- Normalize the child object into child. + SELECT * INTO child FROM objects + INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + + --- Fetch the author's AS2 following collection. + SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + + --- Prepare valid recipients array. + valid_recipients := ARRAY[actor, public]; + IF ARRAY[author_fa] && actor_user.following THEN + valid_recipients := valid_recipients || author_fa; + END IF; + + --- Check visibility. + IF NOT valid_recipients && activity.recipients THEN + --- activity not visible, break out of the loop + RETURN false; + END IF; + + --- If there's a parent, load it and do this all over again. + IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN + SELECT * INTO activity FROM activities + INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE child.data->>'inReplyTo' = objects.data->>'id'; + ELSE + RETURN true; + END IF; + END LOOP; + END; + $$ LANGUAGE plpgsql IMMUTABLE; + """ + + execute(statement) + end + + def down do + execute("drop function thread_visibility(actor varchar, activity_id varchar)") + end +end diff --git a/test/conversation_test.exs b/test/conversation_test.exs index 864b2eb03..5903d10ff 100644 --- a/test/conversation_test.exs +++ b/test/conversation_test.exs @@ -11,6 +11,26 @@ defmodule Pleroma.ConversationTest do import Pleroma.Factory + test "it goes through old direct conversations" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"visibility" => "direct", "status" => "hey @#{other_user.nickname}"}) + + Repo.delete_all(Conversation) + Repo.delete_all(Conversation.Participation) + + refute Repo.one(Conversation) + + Conversation.bump_for_all_activities() + + assert Repo.one(Conversation) + [participation, _p2] = Repo.all(Conversation.Participation) + + assert participation.read + end + test "it creates a conversation for given ap_id" do assert {:ok, %Conversation{} = conversation} = Conversation.create_for_ap_id("https://some_ap_id") diff --git a/test/formatter_test.exs b/test/formatter_test.exs index 06f4f6e50..5e7011160 100644 --- a/test/formatter_test.exs +++ b/test/formatter_test.exs @@ -125,7 +125,7 @@ test "gives a replacement for user links, using local nicknames in user links te archaeme = insert(:user, %{ nickname: "archa_eme_", - info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}} + info: %User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}} }) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs new file mode 100644 index 000000000..ce5d77ff7 --- /dev/null +++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs @@ -0,0 +1,55 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Config + alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug + alias Pleroma.User + + test "it halts if not public and no user is assigned", %{conn: conn} do + set_public_to(false) + + conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + assert conn.status == 403 + assert conn.halted == true + end + + test "it continues if public", %{conn: conn} do + set_public_to(true) + + ret_conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + assert ret_conn == conn + end + + test "it continues if a user is assigned, even if not public", %{conn: conn} do + set_public_to(false) + + conn = + conn + |> assign(:user, %User{}) + + ret_conn = + conn + |> EnsurePublicOrAuthenticatedPlug.call(%{}) + + assert ret_conn == conn + end + + defp set_public_to(value) do + orig = Config.get!([:instance, :public]) + Config.put([:instance, :public], value) + + on_exit(fn -> + Config.put([:instance, :public], orig) + end) + end +end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 0cbb7e4b1..7dfd50c1f 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -7,28 +7,89 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do alias Pleroma.Config alias Plug.Conn - test "it sends CSP headers when enabled", %{conn: conn} do - Config.put([:http_security, :enabled], true) + describe "http security enabled" do + setup do + enabled = Config.get([:http_securiy, :enabled]) - conn = - conn - |> get("/api/v1/instance") + Config.put([:http_security, :enabled], true) - refute Conn.get_resp_header(conn, "x-xss-protection") == [] - refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] - refute Conn.get_resp_header(conn, "x-frame-options") == [] - refute Conn.get_resp_header(conn, "x-content-type-options") == [] - refute Conn.get_resp_header(conn, "x-download-options") == [] - refute Conn.get_resp_header(conn, "referrer-policy") == [] - refute Conn.get_resp_header(conn, "content-security-policy") == [] + on_exit(fn -> + Config.put([:http_security, :enabled], enabled) + end) + + :ok + end + + test "it sends CSP headers when enabled", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + refute Conn.get_resp_header(conn, "x-xss-protection") == [] + refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + refute Conn.get_resp_header(conn, "x-frame-options") == [] + refute Conn.get_resp_header(conn, "x-content-type-options") == [] + refute Conn.get_resp_header(conn, "x-download-options") == [] + refute Conn.get_resp_header(conn, "referrer-policy") == [] + refute Conn.get_resp_header(conn, "content-security-policy") == [] + end + + test "it sends STS headers when enabled", %{conn: conn} do + Config.put([:http_security, :sts], true) + + conn = get(conn, "/api/v1/instance") + + refute Conn.get_resp_header(conn, "strict-transport-security") == [] + refute Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "it does not send STS headers when disabled", %{conn: conn} do + Config.put([:http_security, :sts], false) + + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "strict-transport-security") == [] + assert Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "referrer-policy header reflects configured value", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] + + Config.put([:http_security, :referrer_policy], "no-referrer") + + conn = + build_conn() + |> get("/api/v1/instance") + + assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] + end + + test "it sends `report-to` & `report-uri` CSP response headers" do + conn = + build_conn() + |> get("/api/v1/instance") + + [csp] = Conn.get_resp_header(conn, "content-security-policy") + + assert csp =~ ~r|report-uri https://endpoint.com; report-to csp-endpoint;| + + [reply_to] = Conn.get_resp_header(conn, "reply-to") + + assert reply_to == + "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" + end end test "it does not send CSP headers when disabled", %{conn: conn} do + enabled = Config.get([:http_securiy, :enabled]) + Config.put([:http_security, :enabled], false) - conn = - conn - |> get("/api/v1/instance") + on_exit(fn -> + Config.put([:http_security, :enabled], enabled) + end) + + conn = get(conn, "/api/v1/instance") assert Conn.get_resp_header(conn, "x-xss-protection") == [] assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] @@ -38,46 +99,4 @@ test "it does not send CSP headers when disabled", %{conn: conn} do assert Conn.get_resp_header(conn, "referrer-policy") == [] assert Conn.get_resp_header(conn, "content-security-policy") == [] end - - test "it sends STS headers when enabled", %{conn: conn} do - Config.put([:http_security, :enabled], true) - Config.put([:http_security, :sts], true) - - conn = - conn - |> get("/api/v1/instance") - - refute Conn.get_resp_header(conn, "strict-transport-security") == [] - refute Conn.get_resp_header(conn, "expect-ct") == [] - end - - test "it does not send STS headers when disabled", %{conn: conn} do - Config.put([:http_security, :enabled], true) - Config.put([:http_security, :sts], false) - - conn = - conn - |> get("/api/v1/instance") - - assert Conn.get_resp_header(conn, "strict-transport-security") == [] - assert Conn.get_resp_header(conn, "expect-ct") == [] - end - - test "referrer-policy header reflects configured value", %{conn: conn} do - Config.put([:http_security, :enabled], true) - - conn = - conn - |> get("/api/v1/instance") - - assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] - - Config.put([:http_security, :referrer_policy], "no-referrer") - - conn = - build_conn() - |> get("/api/v1/instance") - - assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] - end end diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs index 6a00dd4fd..efd811df7 100644 --- a/test/plugs/http_signature_plug_test.exs +++ b/test/plugs/http_signature_plug_test.exs @@ -4,7 +4,6 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do use Pleroma.Web.ConnCase - alias Pleroma.Web.HTTPSignatures alias Pleroma.Web.Plugs.HTTPSignaturePlug import Plug.Conn diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs index 8b0b06772..02f530058 100644 --- a/test/plugs/legacy_authentication_plug_test.exs +++ b/test/plugs/legacy_authentication_plug_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase alias Pleroma.Plugs.LegacyAuthenticationPlug alias Pleroma.User diff --git a/test/repo_test.exs b/test/repo_test.exs index 5382289c7..85085a1fa 100644 --- a/test/repo_test.exs +++ b/test/repo_test.exs @@ -1,23 +1,24 @@ defmodule Pleroma.RepoTest do use Pleroma.DataCase import Pleroma.Factory + alias Pleroma.User describe "find_resource/1" do test "returns user" do user = insert(:user) - query = from(t in Pleroma.User, where: t.id == ^user.id) + query = from(t in User, where: t.id == ^user.id) assert Repo.find_resource(query) == {:ok, user} end test "returns not_found" do - query = from(t in Pleroma.User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") + query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") assert Repo.find_resource(query) == {:error, :not_found} end end describe "get_assoc/2" do test "get assoc from preloaded data" do - user = %Pleroma.User{name: "Agent Smith"} + user = %User{name: "Agent Smith"} token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} assert Repo.get_assoc(token, :user) == {:ok, user} end diff --git a/test/support/factory.ex b/test/support/factory.ex index 2a2954ad6..be6247ca4 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Factory do use ExMachina.Ecto, repo: Pleroma.Repo + alias Pleroma.User def participation_factory do conversation = insert(:conversation) @@ -23,7 +24,7 @@ def conversation_factory do end def user_factory do - user = %Pleroma.User{ + user = %User{ name: sequence(:name, &"Test テスト User #{&1}"), email: sequence(:email, &"user#{&1}@example.com"), nickname: sequence(:nickname, &"nick#{&1}"), @@ -34,16 +35,16 @@ def user_factory do %{ user - | ap_id: Pleroma.User.ap_id(user), - follower_address: Pleroma.User.ap_followers(user), - following: [Pleroma.User.ap_id(user)] + | ap_id: User.ap_id(user), + follower_address: User.ap_followers(user), + following: [User.ap_id(user)] } end def note_factory(attrs \\ %{}) do text = sequence(:text, &"This is :moominmamma: note #{&1}") - user = insert(:user) + user = attrs[:user] || insert(:user) data = %{ "type" => "Note", @@ -113,7 +114,8 @@ def direct_note_activity_factory do end def note_activity_factory(attrs \\ %{}) do - note = attrs[:note] || insert(:note) + user = attrs[:user] || insert(:user) + note = attrs[:note] || insert(:note, user: user) data = %{ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs new file mode 100644 index 000000000..579130b05 --- /dev/null +++ b/test/tasks/database_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.DatabaseTest do + alias Pleroma.Repo + alias Pleroma.User + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running update_users_following_followers_counts" do + test "following and followers count are updated" do + [user, user2] = insert_pair(:user) + {:ok, %User{following: following, info: info} = user} = User.follow(user, user2) + + assert length(following) == 2 + assert info.follower_count == 0 + + info_cng = Ecto.Changeset.change(info, %{follower_count: 3}) + + {:ok, user} = + user + |> Ecto.Changeset.change(%{following: following ++ following}) + |> Ecto.Changeset.put_embed(:info, info_cng) + |> Repo.update() + + assert length(user.following) == 4 + assert user.info.follower_count == 3 + + assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"]) + + user = User.get_by_id(user.id) + + assert length(user.following) == 2 + assert user.info.follower_count == 0 + end + end +end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index eaf4ecf84..260ce0d95 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.UserTest do + alias Pleroma.Repo alias Pleroma.User use Pleroma.DataCase @@ -338,4 +339,31 @@ test "activities are deleted" do assert message == "User #{nickname} statuses deleted." end end + + describe "running toggle_confirmed" do + test "user is confirmed" do + %{id: id, nickname: nickname} = insert(:user, info: %{confirmation_pending: false}) + + assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "#{nickname} needs confirmation." + + user = Repo.get(User, id) + assert user.info.confirmation_pending + assert user.info.confirmation_token + end + + test "user is not confirmed" do + %{id: id, nickname: nickname} = + insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"}) + + assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "#{nickname} doesn't need confirmation." + + user = Repo.get(User, id) + refute user.info.confirmation_pending + refute user.info.confirmation_token + end + end end diff --git a/test/user_test.exs b/test/user_test.exs index 0b65e89e9..10e463ff8 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -277,7 +277,7 @@ test "it requires an email, name, nickname and password, bio is optional" do end test "it restricts certain nicknames" do - [restricted_name | _] = Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) + [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) assert is_bitstring(restricted_name) @@ -626,6 +626,37 @@ test "it sets the info->follower_count property" do end end + describe "remove duplicates from following list" do + test "it removes duplicates" do + user = insert(:user) + follower = insert(:user) + + {:ok, %User{following: following} = follower} = User.follow(follower, user) + assert length(following) == 2 + + {:ok, follower} = + follower + |> User.update_changeset(%{following: following ++ following}) + |> Repo.update() + + assert length(follower.following) == 4 + + {:ok, follower} = User.remove_duplicated_following(follower) + assert length(follower.following) == 2 + end + + test "it does nothing when following is uniq" do + user = insert(:user) + follower = insert(:user) + + {:ok, follower} = User.follow(follower, user) + assert length(follower.following) == 2 + + {:ok, follower} = User.remove_duplicated_following(follower) + assert length(follower.following) == 2 + end + end + describe "follow_import" do test "it imports user followings from list" do [user1, user2, user3] = insert_list(3, :user) @@ -873,7 +904,6 @@ test "hide a user's statuses from timelines and notifications" do assert [activity] == ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) - |> ActivityPub.contain_timeline(user2) {:ok, _user} = User.deactivate(user) @@ -882,7 +912,6 @@ test "hide a user's statuses from timelines and notifications" do assert [] == ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) - |> ActivityPub.contain_timeline(user2) end end @@ -1194,14 +1223,32 @@ test "follower count is updated when a follower is blocked" do follower2 = insert(:user) follower3 = insert(:user) - {:ok, follower} = Pleroma.User.follow(follower, user) - {:ok, _follower2} = Pleroma.User.follow(follower2, user) - {:ok, _follower3} = Pleroma.User.follow(follower3, user) + {:ok, follower} = User.follow(follower, user) + {:ok, _follower2} = User.follow(follower2, user) + {:ok, _follower3} = User.follow(follower3, user) - {:ok, _} = Pleroma.User.block(user, follower) + {:ok, _} = User.block(user, follower) user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user}) assert Map.get(user_show, "followers_count") == 2 end + + describe "toggle_confirmation/1" do + test "if user is confirmed" do + user = insert(:user, info: %{confirmation_pending: false}) + {:ok, user} = User.toggle_confirmation(user) + + assert user.info.confirmation_pending + assert user.info.confirmation_token + end + + test "if user is unconfirmed" do + user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"}) + {:ok, user} = User.toggle_confirmation(user) + + refute user.info.confirmation_pending + refute user.info.confirmation_token + end + end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 0f90aa1ac..c18e0ab5f 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -462,6 +462,29 @@ test "doesn't return announce activities concerning blocked users" do refute Enum.member?(activities, activity_three.id) end + test "doesn't return activities from blocked domains" do + domain = "dogwhistle.zone" + domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) + note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) + activity = insert(:note_activity, %{note: note}) + user = insert(:user) + {:ok, user} = User.block_domain(user, domain) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + + refute activity in activities + + followed_user = insert(:user) + ActivityPub.follow(user, followed_user) + {:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => user, "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) @@ -960,17 +983,21 @@ test "it filters broken threads" do "in_reply_to_status_id" => private_activity_2.id }) - activities = ActivityPub.fetch_activities([user1.ap_id | user1.following]) + activities = + ActivityPub.fetch_activities([user1.ap_id | user1.following]) + |> Enum.map(fn a -> a.id end) private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) - assert [public_activity, private_activity_1, private_activity_3] == activities + assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities assert length(activities) == 3 - activities = ActivityPub.contain_timeline(activities, user1) + activities = + ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1}) + |> Enum.map(fn a -> a.id end) - assert [public_activity, private_activity_1] == activities + assert [public_activity.id, private_activity_1.id] == activities assert length(activities) == 2 end end diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs new file mode 100644 index 000000000..1e0511975 --- /dev/null +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -0,0 +1,192 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF.SimplePolicy + + setup do + orig = Config.get!(:mrf_simple) + + Config.put(:mrf_simple, + media_removal: [], + media_nsfw: [], + federated_timeline_removal: [], + reject: [], + accept: [] + ) + + on_exit(fn -> + Config.put(:mrf_simple, orig) + end) + end + + describe "when :media_removal" do + test "is empty" do + Config.put([:mrf_simple, :media_removal], []) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == {:ok, media_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :media_removal], ["remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> Map.put("object", Map.delete(media_message["object"], "attachment"))} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + describe "when :media_nsfw" do + test "is empty" do + Config.put([:mrf_simple, :media_nsfw], []) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == {:ok, media_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :media_nsfw], ["remote.instance"]) + media_message = build_media_message() + local_message = build_local_message() + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> put_in(["object", "tag"], ["foo", "nsfw"]) + |> put_in(["object", "sensitive"], true)} + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + defp build_media_message do + %{ + "actor" => "https://remote.instance/users/bob", + "type" => "Create", + "object" => %{ + "attachment" => [%{}], + "tag" => ["foo"], + "sensitive" => false + } + } + end + + describe "when :federated_timeline_removal" do + test "is empty" do + Config.put([:mrf_simple, :federated_timeline_removal], []) + {_, ftl_message} = build_ftl_actor_and_message() + local_message = build_local_message() + + assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message} + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + + test "has a matching host" do + {actor, ftl_message} = build_ftl_actor_and_message() + + ftl_message_actor_host = + ftl_message + |> Map.fetch!("actor") + |> URI.parse() + |> Map.fetch!(:host) + + Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host]) + local_message = build_local_message() + + assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message) + assert actor.follower_address in ftl_message["to"] + refute actor.follower_address in ftl_message["cc"] + refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"] + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + end + end + + defp build_ftl_actor_and_message do + actor = insert(:user) + + {actor, + %{ + "actor" => actor.ap_id, + "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"], + "cc" => [actor.follower_address, "http://foo.bar/qux"] + }} + end + + describe "when :reject" do + test "is empty" do + Config.put([:mrf_simple, :reject], []) + + remote_message = build_remote_message() + + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "has a matching host" do + Config.put([:mrf_simple, :reject], ["remote.instance"]) + + remote_message = build_remote_message() + + assert SimplePolicy.filter(remote_message) == {:reject, nil} + end + end + + describe "when :accept" do + test "is empty" do + Config.put([:mrf_simple, :accept], []) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + + test "is not empty but it doesn't have a matching host" do + Config.put([:mrf_simple, :accept], ["non.matching.remote"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:reject, nil} + end + + test "has a matching host" do + Config.put([:mrf_simple, :accept], ["remote.instance"]) + + local_message = build_local_message() + remote_message = build_remote_message() + + assert SimplePolicy.filter(local_message) == {:ok, local_message} + assert SimplePolicy.filter(remote_message) == {:ok, remote_message} + end + end + + defp build_local_message do + %{ + "actor" => "#{Pleroma.Web.base_url()}/users/alice", + "to" => [], + "cc" => [] + } + end + + defp build_remote_message do + %{"actor" => "https://remote.instance/users/bob"} + end +end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index 24b96c4aa..e2584f635 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -95,4 +95,26 @@ test "visible_for_user?", %{ refute Visibility.visible_for_user?(private, unrelated) refute Visibility.visible_for_user?(direct, unrelated) end + + test "doesn't die when the user doesn't exist", + %{ + direct: direct, + user: user + } do + Repo.delete(user) + Cachex.clear(:user_cache) + refute Visibility.is_private?(direct) + end + + test "get_visibility", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted + } do + assert Visibility.get_visibility(public) == "public" + assert Visibility.get_visibility(private) == "private" + assert Visibility.get_visibility(direct) == "direct" + assert Visibility.get_visibility(unlisted) == "unlisted" + end end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 6c1897b5a..ca12c7215 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -5,8 +5,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.Activity alias Pleroma.User alias Pleroma.UserInviteToken + alias Pleroma.Web.CommonAPI import Pleroma.Factory describe "/api/pleroma/admin/users" do @@ -949,4 +951,329 @@ test "with token" do } end end + + describe "GET /api/pleroma/admin/reports/:id" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + %{conn: assign(conn, :user, admin)} + end + + test "returns report by its id", %{conn: conn} 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 + |> get("/api/pleroma/admin/reports/#{report_id}") + |> json_response(:ok) + + assert response["id"] == report_id + end + + test "returns 404 when report id is invalid", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/reports/test") + + assert json_response(conn, :not_found) == "Not found" + end + end + + describe "PUT /api/pleroma/admin/reports/:id" do + setup %{conn: conn} do + admin = insert(:user, info: %{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] + }) + + %{conn: assign(conn, :user, admin), id: report_id} + end + + test "mark report as resolved", %{conn: conn, id: id} do + response = + conn + |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) + |> json_response(:ok) + + assert response["state"] == "resolved" + end + + test "closes report", %{conn: conn, id: id} do + response = + conn + |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) + |> json_response(:ok) + + assert response["state"] == "closed" + end + + test "returns 400 when state is unknown", %{conn: conn, id: id} do + conn = + conn + |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"}) + + assert json_response(conn, :bad_request) == "Unsupported state" + end + + test "returns 404 when report is not exist", %{conn: conn} do + conn = + conn + |> put("/api/pleroma/admin/reports/test", %{"state" => "closed"}) + + assert json_response(conn, :not_found) == "Not found" + end + end + + describe "GET /api/pleroma/admin/reports" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + %{conn: assign(conn, :user, admin)} + end + + test "returns empty response when no reports created", %{conn: conn} do + response = + conn + |> get("/api/pleroma/admin/reports") + |> json_response(:ok) + + assert Enum.empty?(response["reports"]) + end + + test "returns reports", %{conn: conn} 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 + |> get("/api/pleroma/admin/reports") + |> json_response(:ok) + + [report] = response["reports"] + + assert length(response["reports"]) == 1 + assert report["id"] == report_id + end + + test "returns reports with specified state", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %{id: first_report_id}} = + CommonAPI.report(reporter, %{ + "account_id" => target_user.id, + "comment" => "I feel offended", + "status_ids" => [activity.id] + }) + + {:ok, %{id: second_report_id}} = + CommonAPI.report(reporter, %{ + "account_id" => target_user.id, + "comment" => "I don't like this user" + }) + + CommonAPI.update_report_state(second_report_id, "closed") + + response = + conn + |> get("/api/pleroma/admin/reports", %{ + "state" => "open" + }) + |> json_response(:ok) + + [open_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert open_report["id"] == first_report_id + + response = + conn + |> get("/api/pleroma/admin/reports", %{ + "state" => "closed" + }) + |> json_response(:ok) + + [closed_report] = response["reports"] + + assert length(response["reports"]) == 1 + assert closed_report["id"] == second_report_id + + response = + conn + |> get("/api/pleroma/admin/reports", %{ + "state" => "resolved" + }) + |> json_response(:ok) + + assert Enum.empty?(response["reports"]) + end + + test "returns 403 when requested by a non-admin" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> get("/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} + end + + test "returns 403 when requested by anonymous" do + conn = + build_conn() + |> get("/api/pleroma/admin/reports") + + assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."} + end + end + + describe "POST /api/pleroma/admin/reports/:id/respond" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + %{conn: assign(conn, :user, admin)} + end + + test "returns created dm", %{conn: conn} 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 conn.assigns[:user].nickname in recipients + assert reporter.nickname in recipients + assert response["content"] == "I will check it out" + assert response["visibility"] == "direct" + 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, info: %{is_admin: true}) + activity = insert(:note_activity) + + %{conn: assign(conn, :user, admin), id: activity.id} + end + + test "toggle sensitive flag", %{conn: conn, id: id} do + response = + conn + |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) + |> json_response(:ok) + + assert response["sensitive"] + + response = + conn + |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) + |> json_response(:ok) + + refute response["sensitive"] + end + + test "change visibility flag", %{conn: conn, id: id} do + response = + conn + |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"}) + |> json_response(:ok) + + assert response["visibility"] == "public" + + response = + conn + |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"}) + |> json_response(:ok) + + assert response["visibility"] == "private" + + response = + conn + |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "unlisted"}) + |> json_response(:ok) + + assert response["visibility"] == "unlisted" + end + + test "returns 400 when visibility is unknown", %{conn: conn, id: id} do + conn = + conn + |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"}) + + assert json_response(conn, :bad_request) == "Unsupported visibility" + end + end + + describe "DELETE /api/pleroma/admin/statuses/:id" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + activity = insert(:note_activity) + + %{conn: assign(conn, :user, admin), id: activity.id} + end + + test "deletes status", %{conn: conn, id: id} do + conn + |> delete("/api/pleroma/admin/statuses/#{id}") + |> json_response(:ok) + + refute Activity.get_by_id(id) + end + + test "returns error when status is not exist", %{conn: conn} do + conn = + conn + |> delete("/api/pleroma/admin/statuses/test") + + assert json_response(conn, :bad_request) == "Could not delete" + end + end end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index a5b07c446..696060fb1 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -87,6 +87,28 @@ test "it filters out obviously bad tags when accepting a post as Markdown" do assert object.data["content"] == "

2hu

alert('xss')" end + + test "it does not allow replies to direct messages that are not direct messages themselves" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"}) + + assert {:ok, _} = + CommonAPI.post(user, %{ + "status" => "suya..", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id + }) + + Enum.each(["public", "private", "unlisted"], fn visibility -> + assert {:error, {:private_to_public, _}} = + CommonAPI.post(user, %{ + "status" => "suya..", + "visibility" => visibility, + "in_reply_to_status_id" => activity.id + }) + end) + end end describe "reactions" do @@ -239,10 +261,41 @@ test "creates a report" do data: %{ "type" => "Flag", "content" => ^comment, - "object" => [^target_ap_id, ^activity_ap_id] + "object" => [^target_ap_id, ^activity_ap_id], + "state" => "open" } } = flag_activity end + + test "updates report state" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id}} = + CommonAPI.report(reporter, %{ + "account_id" => target_user.id, + "comment" => "I feel offended", + "status_ids" => [activity.id] + }) + + {:ok, report} = CommonAPI.update_report_state(report_id, "resolved") + + assert report.data["state"] == "resolved" + end + + test "does not update report state when state is unsupported" do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id}} = + CommonAPI.report(reporter, %{ + "account_id" => target_user.id, + "comment" => "I feel offended", + "status_ids" => [activity.id] + }) + + assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} + end end describe "reblog muting" do @@ -257,14 +310,14 @@ test "creates a report" do test "add a reblog mute", %{muter: muter, muted: muted} do {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) - assert Pleroma.User.showing_reblogs?(muter, muted) == false + assert User.showing_reblogs?(muter, muted) == false end test "remove a reblog mute", %{muter: muter, muted: muted} do {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) {:ok, muter} = CommonAPI.show_reblogs(muter, muted) - assert Pleroma.User.showing_reblogs?(muter, muted) == true + assert User.showing_reblogs?(muter, muted) == true end end end diff --git a/test/web/http_sigs/http_sig_test.exs b/test/web/http_sigs/http_sig_test.exs deleted file mode 100644 index c4d2eaf78..000000000 --- a/test/web/http_sigs/http_sig_test.exs +++ /dev/null @@ -1,194 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -# http signatures -# Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C -defmodule Pleroma.Web.HTTPSignaturesTest do - use Pleroma.DataCase - alias Pleroma.Web.HTTPSignatures - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - @public_key hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key"))) - |> :public_key.pem_entry_decode() - - @headers %{ - "(request-target)" => "post /foo?param=value&pet=dog", - "host" => "example.com", - "date" => "Thu, 05 Jan 2014 21:31:40 GMT", - "content-type" => "application/json", - "digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", - "content-length" => "18" - } - - @default_signature """ - keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=" - """ - - @basic_signature """ - keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4=" - """ - - @all_headers_signature """ - keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=" - """ - - test "split up a signature" do - expected = %{ - "keyId" => "Test", - "algorithm" => "rsa-sha256", - "signature" => - "jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=", - "headers" => ["date"] - } - - assert HTTPSignatures.split_signature(@default_signature) == expected - end - - test "validates the default case" do - signature = HTTPSignatures.split_signature(@default_signature) - assert HTTPSignatures.validate(@headers, signature, @public_key) - end - - test "validates the basic case" do - signature = HTTPSignatures.split_signature(@basic_signature) - assert HTTPSignatures.validate(@headers, signature, @public_key) - end - - test "validates the all-headers case" do - signature = HTTPSignatures.split_signature(@all_headers_signature) - assert HTTPSignatures.validate(@headers, signature, @public_key) - end - - test "it contructs a signing string" do - expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18" - assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"]) - end - - test "it validates a conn" do - public_key_pem = - "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGb42rPZIapY4Hfhxrgn\nxKVJczBkfDviCrrYaYjfGxawSw93dWTUlenCVTymJo8meBlFgIQ70ar4rUbzl6GX\nMYvRdku072d1WpglNHXkjKPkXQgngFDrh2sGKtNB/cEtJcAPRO8OiCgPFqRtMiNM\nc8VdPfPdZuHEIZsJ/aUM38EnqHi9YnVDQik2xxDe3wPghOhqjxUM6eLC9jrjI+7i\naIaEygUdyst9qVg8e2FGQlwAeS2Eh8ygCxn+bBlT5OyV59jSzbYfbhtF2qnWHtZy\nkL7KOOwhIfGs7O9SoR2ZVpTEQ4HthNzainIe/6iCR5HGrao/T8dygweXFYRv+k5A\nPQIDAQAB\n-----END PUBLIC KEY-----\n" - - [public_key] = :public_key.pem_decode(public_key_pem) - - public_key = - public_key - |> :public_key.pem_entry_decode() - - conn = %{ - req_headers: [ - {"host", "localtesting.pleroma.lol"}, - {"connection", "close"}, - {"content-length", "2316"}, - {"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"}, - {"date", "Sun, 10 Dec 2017 14:23:49 GMT"}, - {"digest", "SHA-256=x/bHADMW8qRrq2NdPb5P9fl0lYpKXXpe5h5maCIL0nM="}, - {"content-type", "application/activity+json"}, - {"(request-target)", "post /users/demiurge/inbox"}, - {"signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"i0FQvr51sj9BoWAKydySUAO1RDxZmNY6g7M62IA7VesbRSdFZZj9/fZapLp6YSuvxUF0h80ZcBEq9GzUDY3Chi9lx6yjpUAS2eKb+Am/hY3aswhnAfYd6FmIdEHzsMrpdKIRqO+rpQ2tR05LwiGEHJPGS0p528NvyVxrxMT5H5yZS5RnxY5X2HmTKEgKYYcvujdv7JWvsfH88xeRS7Jlq5aDZkmXvqoR4wFyfgnwJMPLel8P/BUbn8BcXglH/cunR0LUP7sflTxEz+Rv5qg+9yB8zgBsB4C0233WpcJxjeD6Dkq0EcoJObBR56F8dcb7NQtUDu7x6xxzcgSd7dHm5w==\""} - ] - } - - assert HTTPSignatures.validate_conn(conn, public_key) - end - - test "it validates a conn and fetches the key" do - conn = %{ - params: %{"actor" => "http://mastodon.example.org/users/admin"}, - req_headers: [ - {"host", "localtesting.pleroma.lol"}, - {"x-forwarded-for", "127.0.0.1"}, - {"connection", "close"}, - {"content-length", "2307"}, - {"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"}, - {"date", "Sun, 11 Feb 2018 17:12:01 GMT"}, - {"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="}, - {"content-type", "application/activity+json"}, - {"signature", - "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""}, - {"(request-target)", "post /users/demiurge/inbox"} - ] - } - - assert HTTPSignatures.validate_conn(conn) - end - - test "validate this" do - conn = %{ - params: %{"actor" => "https://niu.moe/users/rye"}, - req_headers: [ - {"x-forwarded-for", "149.202.73.191"}, - {"host", "testing.pleroma.lol"}, - {"x-cluster-client-ip", "149.202.73.191"}, - {"connection", "upgrade"}, - {"content-length", "2396"}, - {"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"}, - {"date", "Sun, 18 Feb 2018 20:31:51 GMT"}, - {"digest", "SHA-256=dzH+vLyhxxALoe9RJdMl4hbEV9bGAZnSfddHQzeidTU="}, - {"content-type", "application/activity+json"}, - {"signature", - "keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"wtxDg4kIpW7nsnUcVJhBk6SgJeDZOocr8yjsnpDRqE52lR47SH6X7G16r7L1AUJdlnbfx7oqcvomoIJoHB3ghP6kRnZW6MyTMZ2jPoi3g0iC5RDqv6oAmDSO14iw6U+cqZbb3P/odS5LkbThF0UNXcfenVNfsKosIJycFjhNQc54IPCDXYq/7SArEKJp8XwEgzmiC2MdxlkVIUSTQYfjM4EG533cwlZocw1mw72e5mm/owTa80BUZAr0OOuhoWARJV9btMb02ZyAF6SCSoGPTA37wHyfM1Dk88NHf7Z0Aov/Fl65dpRM+XyoxdkpkrhDfH9qAx4iuV2VEWddQDiXHA==\""}, - {"(request-target)", "post /inbox"} - ] - } - - assert HTTPSignatures.validate_conn(conn) - end - - test "validate this too" do - conn = %{ - params: %{"actor" => "https://niu.moe/users/rye"}, - req_headers: [ - {"x-forwarded-for", "149.202.73.191"}, - {"host", "testing.pleroma.lol"}, - {"x-cluster-client-ip", "149.202.73.191"}, - {"connection", "upgrade"}, - {"content-length", "2342"}, - {"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"}, - {"date", "Sun, 18 Feb 2018 21:44:46 GMT"}, - {"digest", "SHA-256=vS8uDOJlyAu78cF3k5EzrvaU9iilHCX3chP37gs5sS8="}, - {"content-type", "application/activity+json"}, - {"signature", - "keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"IN6fHD8pLiDEf35dOaRHzJKc1wBYh3/Yq0ItaNGxUSbJTd2xMjigZbcsVKzvgYYjglDDN+disGNeD+OBKwMqkXWaWe/lyMc9wHvCH5NMhpn/A7qGLY8yToSt4vh8ytSkZKO6B97yC+Nvy6Fz/yMbvKtFycIvSXCq417cMmY6f/aG+rtMUlTbKO5gXzC7SUgGJCtBPCh1xZzu5/w0pdqdjO46ePNeR6JyJSLLV4hfo3+p2n7SRraxM4ePVCUZqhwS9LPt3Zdhy3ut+IXCZgMVIZggQFM+zXLtcXY5HgFCsFQr5WQDu+YkhWciNWtKFnWfAsnsg5sC330lZ/0Z8Z91yA==\""}, - {"(request-target)", "post /inbox"} - ] - } - - assert HTTPSignatures.validate_conn(conn) - end - - test "it generates a signature" do - user = insert(:user) - assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\"" - end - - test "this too" do - conn = %{ - params: %{"actor" => "https://mst3k.interlinked.me/users/luciferMysticus"}, - req_headers: [ - {"host", "soc.canned-death.us"}, - {"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://mst3k.interlinked.me/)"}, - {"date", "Sun, 11 Mar 2018 12:19:36 GMT"}, - {"digest", "SHA-256=V7Hl6qDK2m8WzNsjzNYSBISi9VoIXLFlyjF/a5o1SOc="}, - {"content-type", "application/activity+json"}, - {"signature", - "keyId=\"https://mst3k.interlinked.me/users/luciferMysticus#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"CTYdK5a6lYMxzmqjLOpvRRASoxo2Rqib2VrAvbR5HaTn80kiImj15pCpAyx8IZp53s0Fn/y8MjCTzp+absw8kxx0k2sQAXYs2iy6xhdDUe7iGzz+XLAEqLyZIZfecynaU2nb3Z2XnFDjhGjR1vj/JP7wiXpwp6o1dpDZj+KT2vxHtXuB9585V+sOHLwSB1cGDbAgTy0jx/2az2EGIKK2zkw1KJuAZm0DDMSZalp/30P8dl3qz7DV2EHdDNfaVtrs5BfbDOZ7t1hCcASllzAzgVGFl0BsrkzBfRMeUMRucr111ZG+c0BNOEtJYOHSyZsSSdNknElggCJekONYMYk5ZA==\""}, - {"x-forwarded-for", "2607:5300:203:2899::31:1337"}, - {"x-forwarded-host", "soc.canned-death.us"}, - {"x-forwarded-server", "soc.canned-death.us"}, - {"connection", "Keep-Alive"}, - {"content-length", "2006"}, - {"(request-target)", "post /inbox"} - ] - } - - assert HTTPSignatures.validate_conn(conn) - end -end diff --git a/test/web/http_sigs/priv.key b/test/web/http_sigs/priv.key deleted file mode 100644 index 425518a06..000000000 --- a/test/web/http_sigs/priv.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF -NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F -UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB -AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA -QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK -kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg -f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u -412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc -mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 -kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA -gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW -G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI -7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== ------END RSA PRIVATE KEY----- diff --git a/test/web/http_sigs/pub.key b/test/web/http_sigs/pub.key deleted file mode 100644 index b3bbf6cb9..000000000 --- a/test/web/http_sigs/pub.key +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 -6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 -Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw -oYi+1hqp1fIekaxsyQIDAQAB ------END PUBLIC KEY----- diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 5c79ee633..cbff141c8 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -81,6 +81,19 @@ test "the public timeline", %{conn: conn} do end) end + test "the public timeline when public is set to false", %{conn: conn} do + public = Pleroma.Config.get([:instance, :public]) + Pleroma.Config.put([:instance, :public], false) + + on_exit(fn -> + Pleroma.Config.put([:instance, :public], public) + end) + + assert conn + |> get("/api/v1/timelines/public", %{"local" => "False"}) + |> json_response(403) == %{"error" => "This resource requires authentication."} + end + test "posting a status", %{conn: conn} do user = insert(:user) @@ -433,7 +446,7 @@ test "verify_credentials", %{conn: conn} do end test "verify_credentials default scope unlisted", %{conn: conn} do - user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "unlisted"}}) + user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}}) conn = conn @@ -1309,7 +1322,7 @@ test "returns the relationships for the current user", %{conn: conn} do describe "locked accounts" do test "/api/v1/follow_requests works" do - user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) + user = insert(:user, %{info: %User.Info{locked: true}}) other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) @@ -1354,7 +1367,7 @@ test "/api/v1/follow_requests/:id/authorize works" do end test "verify_credentials", %{conn: conn} do - user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "private"}}) + user = insert(:user, %{info: %User.Info{default_scope: "private"}}) conn = conn @@ -1366,7 +1379,7 @@ test "verify_credentials", %{conn: conn} do end test "/api/v1/follow_requests/:id/reject works" do - user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) + user = insert(:user, %{info: %User.Info{locked: true}}) other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) @@ -2116,7 +2129,7 @@ test "returns favorited DM only when user is logged in and he is one of recipien |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> json_response(:ok) - assert length(anonymous_response) == 0 + assert Enum.empty?(anonymous_response) end test "does not return others' favorited DM when user is not one of recipients", %{ @@ -2140,7 +2153,7 @@ test "does not return others' favorited DM when user is not one of recipients", |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> json_response(:ok) - assert length(response) == 0 + assert Enum.empty?(response) end test "paginates favorites using since_id and max_id", %{ diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index 2916caf8d..3516a13fa 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -355,7 +355,7 @@ test "tries to use the information in poco fields" do {:ok, user} = OStatus.find_or_make_user(uri) - user = Pleroma.User.get_cached_by_id(user.id) + user = User.get_cached_by_id(user.id) assert user.name == "Constance Variable" assert user.nickname == "lambadalambda@social.heldscal.la" assert user.local == false @@ -374,7 +374,7 @@ test "find_or_make_user sets all the nessary input fields" do {:ok, user} = OStatus.find_or_make_user(uri) assert user.info == - %Pleroma.User.Info{ + %User.Info{ id: user.info.id, ap_enabled: false, background: %{},