diff --git a/.gitignore b/.gitignore index 082c7491b..9591f9976 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ erl_crash.dump # Prevent committing docs files /priv/static/doc/* + +# Code test coverage +/cover +/Elixir.*.coverdata 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 210aae2e4..b0e849285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 +- 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) - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) +- Mastodon API: `POST /api/v1/accounts` (account creation API) - ActivityPub C2S: OAuth endpoints -- Metadata RelMe provider +- Metadata: RelMe provider - OAuth: added support for refresh tokens - Emoji packs and emoji pack manager @@ -41,8 +44,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Federation: Removed `inReplyToStatusId` from objects - Configuration: Dedupe enabled by default - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. -- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. +- Admin API: Move the user related API to `api/pleroma/admin/users` +- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` - Mastodon API: Provide plaintext versions of cw/content in the Status entity @@ -56,17 +60,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add `with_muted` parameter to timeline endpoints - Mastodon API: Actual reblog hiding instead of a dummy - Mastodon API: Remove attachment limit in the Status entity +- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. - Deps: Updated Cowboy to 2.6 - Deps: Updated Ecto to 3.0.7 - Don't ship finmoji by default, they can be installed as an emoji pack -- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. +- Hide deactivated users and their statuses ### Fixed - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended. - Followers counter not being updated when a follower is blocked - Deactivated users being able to request an access token - Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak -- proper Twitter Card generation instead of a dummy +- Proper Twitter Card generation instead of a dummy - Deletions failing for users with a large number of posts - NodeInfo: Include admins in `staffAccounts` - ActivityPub: Crashing when requesting empty local user's outbox @@ -90,6 +95,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow` - Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON - Mastodon API: Exposing default scope of the user to anyone +- 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` ## [0.9.9999] - 2019-04-05 ### Security diff --git a/COPYING b/COPYING index eceb68efe..0aede0fba 100644 --- a/COPYING +++ b/COPYING @@ -15,6 +15,14 @@ priv/static/images/pleroma-tan.png --- +The following files are copyright © 2019 shitposter.club, and are distributed +under the Creative Commons Attribution 4.0 International license, you should +have received a copy of the license file as CC-BY-4.0. + +priv/static/images/pleroma-fox-tan-shy.png + +--- + The following files are copyright © 2017-2019 Pleroma Authors , and are distributed under the Creative Commons Attribution-ShareAlike 4.0 International license, you should have received diff --git a/README.md b/README.md index 987f973ea..928f75dc7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For clients it supports both the [GNU Social API with Qvitter extensions](https: - [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html) -No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at . +If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at . ## Installation diff --git a/config/config.exs b/config/config.exs index 1e64b79a7..9cbac35eb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -212,6 +212,11 @@ registrations_open: true, federating: true, federation_reachability_timeout_days: 7, + federation_publisher_modules: [ + Pleroma.Web.ActivityPub.Publisher, + Pleroma.Web.Websub, + Pleroma.Web.Salmon + ], allow_relay: true, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, @@ -234,6 +239,8 @@ safe_dm_mentions: false, healthcheck: false +config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800 + config :pleroma, :markup, # XXX - unfortunately, inline images must be enabled by default right now, because # of custom emoji. Issue #275 discusses defanging that somehow. @@ -246,25 +253,6 @@ Pleroma.HTML.Scrubber.Default ] -# Deprecated, will be gone in 1.0 -config :pleroma, :fe, - theme: "pleroma-dark", - logo: "/static/logo.png", - logo_mask: true, - logo_margin: "0.1em", - background: "/static/aurora_borealis.jpg", - redirect_root_no_login: "/main/all", - redirect_root_login: "/main/friends", - show_instance_panel: true, - scope_options_enabled: false, - formatting_options_enabled: false, - collapse_message_with_subject: false, - hide_post_stats: false, - hide_user_stats: false, - scope_copy: true, - subject_line_behavior: "email", - always_show_subject_input: true - config :pleroma, :frontend_configurations, pleroma_fe: %{ theme: "pleroma-dark", @@ -476,6 +464,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 f93bc5994..a0c90c371 100644 --- a/config/test.exs +++ b/config/test.exs @@ -59,6 +59,8 @@ total_user_limit: 3, enabled: false +config :pleroma, :app_account_creation, max_requests: 5 + try do import_config "test.secret.exs" rescue diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 8befa8ea0..75fa2ee83 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -8,15 +8,20 @@ Authentication is required and the user must be an admin. - Method `GET` - Query Params: - - *optional* `query`: **string** search term + - *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain) - *optional* `filters`: **string** comma-separated string of filters: - `local`: only local users - `external`: only external users - `active`: only active users - `deactivated`: only deactivated users + - `is_admin`: users with admin role + - `is_moderator`: users with moderator role - *optional* `page`: **integer** page number - *optional* `page_size`: **integer** number of users per page (default is `50`) -- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10` + - *optional* `tags`: **[string]** tags list + - *optional* `name`: **string** user display name + - *optional* `email`: **string** user email +- 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 @@ -40,7 +45,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/user` +## `/api/pleroma/admin/users` ### Remove a user @@ -58,7 +63,7 @@ Authentication is required and the user must be an admin. - `password` - Response: User’s nickname -## `/api/pleroma/admin/user/follow` +## `/api/pleroma/admin/users/follow` ### Make a user follow another user - Methods: `POST` @@ -68,7 +73,7 @@ Authentication is required and the user must be an admin. - Response: - "ok" -## `/api/pleroma/admin/user/unfollow` +## `/api/pleroma/admin/users/unfollow` ### Make a user unfollow another user - Methods: `POST` @@ -111,7 +116,7 @@ Authentication is required and the user must be an admin. - `nickname` - `tags` -## `/api/pleroma/admin/permission_group/:nickname` +## `/api/pleroma/admin/users/:nickname/permission_group` ### Get user user permission groups membership @@ -126,7 +131,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/permission_group/:nickname/:permission_group` +## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group` Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist. @@ -160,7 +165,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - On success: JSON of the `user.info` - Note: An admin cannot revoke their own admin status. -## `/api/pleroma/admin/activation_status/:nickname` +## `/api/pleroma/admin/users/:nickname/activation_status` ### Active or deactivate a user @@ -198,7 +203,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Response: - On success: URL of the unfollowed relay -## `/api/pleroma/admin/invite_token` +## `/api/pleroma/admin/users/invite_token` ### Get an account registration invite token @@ -210,7 +215,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ] - Response: invite token (base64 string) -## `/api/pleroma/admin/invites` +## `/api/pleroma/admin/users/invites` ### Get a list of generated invites @@ -236,7 +241,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` -## `/api/pleroma/admin/revoke_invite` +## `/api/pleroma/admin/users/revoke_invite` ### Revoke invite by token @@ -259,7 +264,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ``` -## `/api/pleroma/admin/email_invite` +## `/api/pleroma/admin/users/email_invite` ### Sends registration invite via email @@ -268,7 +273,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `email` - `name`, optional -## `/api/pleroma/admin/password_reset` +## `/api/pleroma/admin/users/:nickname/password_reset` ### Get a password reset token for a given nickname diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index d3ba41b6a..36b47608e 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -87,3 +87,13 @@ Additional parameters can be added to the JSON body/Form data: `POST /oauth/token` Post here request with grant_type=refresh_token to obtain new access token. Returns an access token. + +## Account Registration +`POST /api/v1/accounts` + +Has theses additionnal parameters (which are the same as in Pleroma-API): + * `fullname`: optional + * `bio`: optional + * `captcha_solution`: optional, contains provider-specific captcha solution, + * `captcha_token`: optional, contains provider-specific captcha token + * `token`: invite token required when the registerations aren't public. diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 190846de9..dd0b6ca73 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -61,6 +61,15 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi * Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise * Example response: `{"error": "Invalid password."}` +## `/api/pleroma/disable_account` +### Disable an account +* Method `POST` +* Authentication: required +* Params: + * `password`: user's password +* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise +* Example response: `{"error": "Invalid password."}` + ## `/api/account/register` ### Register a new user * Method `POST` diff --git a/docs/config.md b/docs/config.md index 43ea24d80..470f71b7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -105,6 +105,12 @@ config :pleroma, Pleroma.Emails.Mailer, * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`) * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``. +## :app_account_creation +REST API for creating an account settings +* `enabled`: Enable/disable registration +* `max_requests`: Number of requests allowed for creating accounts +* `interval`: Interval for restricting requests for one ip (seconds) + ## :logger * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 5cb54c3ca..d2ddf450a 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -137,7 +137,7 @@ def run(["get-packs" | args]) do ]) ) - files = Tesla.get!(client(), files_url).body |> Poison.decode!() + files = Tesla.get!(client(), files_url).body |> Jason.decode!() IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -239,7 +239,7 @@ def run(["gen-pack", src]) do emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts) - File.write!(files_name, Poison.encode!(emoji_map, pretty: true)) + File.write!(files_name, Jason.encode!(emoji_map, pretty: true)) IO.puts(""" @@ -248,11 +248,11 @@ def run(["gen-pack", src]) do """) if File.exists?("index.json") do - existing_data = File.read!("index.json") |> Poison.decode!() + existing_data = File.read!("index.json") |> Jason.decode!() File.write!( "index.json", - Poison.encode!( + Jason.encode!( Map.merge( existing_data, pack_json @@ -263,14 +263,14 @@ def run(["gen-pack", src]) do IO.puts("index.json file has been update with the #{name} pack") else - File.write!("index.json", Poison.encode!(pack_json, pretty: true)) + File.write!("index.json", Jason.encode!(pack_json, pretty: true)) IO.puts("index.json has been created with the #{name} pack") end end defp fetch_manifest(from) do - Poison.decode!( + Jason.decode!( if String.starts_with?(from, "http") do Tesla.get!(client(), from).body else diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 6a83a8c0d..d130ff8c9 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -138,7 +138,7 @@ def run(["new", nickname, email | rest]) do bio: bio } - changeset = User.register_changeset(%User{}, params, confirmed: true) + changeset = User.register_changeset(%User{}, params, need_confirmation: false) {:ok, _user} = User.register(changeset) Mix.shell().info("User #{nickname} created") diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 2b661edc1..4a0919478 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -132,7 +132,10 @@ def get_by_ap_id_with_object(ap_id) do end def get_by_id(id) do - Repo.get(Activity, id) + Activity + |> where([a], a.id == ^id) + |> restrict_deactivated_users() + |> Repo.one() end def get_by_id_with_object(id) do @@ -200,6 +203,7 @@ def get_all_create_by_object_ap_id(ap_id) do def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do create_by_object_ap_id(ap_id) + |> restrict_deactivated_users() |> Repo.one() end @@ -287,8 +291,41 @@ def all_by_actor_and_id(actor, status_ids) do |> Repo.all() end + def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do + from( + a in Activity, + where: + fragment( + "? ->> 'type' = 'Follow'", + a.data + ), + where: + fragment( + "? ->> 'state' = 'pending'", + a.data + ), + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + a.data, + a.data, + ^ap_id + ) + ) + end + @spec query_by_actor(actor()) :: Ecto.Query.t() def query_by_actor(actor) do from(a in Activity, where: a.actor == ^actor) end + + def restrict_deactivated_users(query) do + from(activity in query, + where: + fragment( + "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", + activity.actor + ) + ) + end end diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex index 61688e778..18931d5a0 100644 --- a/lib/pleroma/captcha/kocaptcha.ex +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -15,7 +15,7 @@ def new do %{error: "Kocaptcha service unavailable"} {:ok, res} -> - json_resp = Poison.decode!(res.body) + json_resp = Jason.decode!(res.body) %{ type: :kocaptcha, diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index 189faa15f..71a47b9fb 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -12,8 +12,12 @@ def get(key), do: get(key, nil) def get([key], default), do: get(key, default) def get([parent_key | keys], default) do - Application.get_env(:pleroma, parent_key) - |> get_in(keys) || default + case :pleroma + |> Application.get_env(parent_key) + |> get_in(keys) do + nil -> default + any -> any + end end def get(key, default) do diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 0345ac19c..240fb1c37 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -5,15 +5,6 @@ defmodule Pleroma.Config.DeprecationWarnings do require Logger - def check_frontend_config_mechanism do - if Pleroma.Config.get(:fe) do - Logger.warn(""" - !!!DEPRECATION WARNING!!! - You are using the old configuration mechanism for the frontend. Please check config.md. - """) - end - end - def check_hellthread_threshold do if Pleroma.Config.get([:mrf_hellthread, :threshold]) do Logger.warn(""" @@ -24,7 +15,6 @@ def check_hellthread_threshold do end def warn do - check_frontend_config_mechanism() check_hellthread_threshold() end end diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 5f6ab902c..238c1acf2 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -47,8 +47,8 @@ def get_for_ap_id(ap_id) do """ def create_or_bump_for(activity, opts \\ []) do with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), - object <- Pleroma.Object.normalize(activity), "Create" <- activity.data["type"], + object <- Pleroma.Object.normalize(activity), "Note" <- object.data["type"], ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do {:ok, conversation} = create_for_ap_id(ap_id) diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index 1d2e0785c..b3319e137 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -77,13 +77,13 @@ def render_activities(activities) do user = User.get_cached_by_ap_id(activity.data["actor"]) object = Object.normalize(activity) - like_count = object["like_count"] || 0 - announcement_count = object["announcement_count"] || 0 + like_count = object.data["like_count"] || 0 + announcement_count = object.data["announcement_count"] || 0 link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> info("#{like_count} likes, #{announcement_count} repeats") <> "i\tfake\t(NULL)\t0\r\n" <> - info(HTML.strip_tags(String.replace(object["content"], "
", "\r"))) + info(HTML.strip_tags(String.replace(object.data["content"], "
", "\r"))) end) |> Enum.join("i\tfake\t(NULL)\t0\r\n") end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index dd274cf6b..844264307 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -33,6 +33,13 @@ def changeset(%Notification{} = notification, attrs) do def for_user_query(user) do Notification |> where(user_id: ^user.id) + |> where( + [n, a], + fragment( + "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", + a.actor + ) + ) |> join(:inner, [n], activity in assoc(n, :activity)) |> join(:left, [n, a], object in Object, on: 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_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/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 9d43732eb..86bc4aa3a 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Token @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @@ -22,18 +23,39 @@ def call(%{params: %{"access_token" => access_token}} = conn, _) do |> assign(:token, token_record) |> assign(:user, user) else - _ -> conn + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end end end def call(conn, _) do - with {:ok, token_str} <- fetch_token_str(conn), - {:ok, user, token_record} <- fetch_user_and_token(token_str) do - conn - |> assign(:token, token_record) - |> assign(:user, user) - else - _ -> conn + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn end end @@ -54,6 +76,16 @@ defp fetch_user_and_token(token) do end end + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + # Gets token from session by :oauth_token key # @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex new file mode 100644 index 000000000..466f64a79 --- /dev/null +++ b/lib/pleroma/plugs/rate_limit_plug.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimitPlug do + import Phoenix.Controller, only: [json: 2] + import Plug.Conn + + def init(opts), do: opts + + def call(conn, opts) do + enabled? = Pleroma.Config.get([:app_account_creation, :enabled]) + + case check_rate(conn, Map.put(opts, :enabled, enabled?)) do + {:ok, _count} -> conn + {:error, _count} -> render_error(conn) + %Plug.Conn{} = conn -> conn + end + end + + defp check_rate(conn, %{enabled: true} = opts) do + max_requests = opts[:max_requests] + bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") + + ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests) + end + + defp check_rate(conn, _), do: conn + + defp render_error(conn) do + conn + |> put_status(:forbidden) + |> json(%{error: "Rate limit exceeded."}) + |> halt() + end +end 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/stats.ex b/lib/pleroma/stats.ex index 2e7d747df..5b242927b 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -34,7 +34,7 @@ def schedule_update do def update_stats do peers = from( - u in Pleroma.User, + u in User, select: fragment("distinct split_part(?, '@', 2)", u.nickname), where: u.local != ^true ) @@ -44,10 +44,13 @@ def update_stats do domain_count = Enum.count(peers) status_query = - from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info)) + from(u in User.Query.build(%{local: true}), + select: fragment("sum((?->>'note_count')::int)", u.info) + ) status_count = Repo.one(status_query) - user_count = Repo.aggregate(User.active_local_user_query(), :count, :id) + + user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) Agent.update(__MODULE__, fn _ -> {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}} diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex index 3046cdbd2..dd44c7561 100644 --- a/lib/pleroma/uploaders/swift/keystone.ex +++ b/lib/pleroma/uploaders/swift/keystone.ex @@ -14,7 +14,7 @@ def process_url(url) do def process_response_body(body) do body - |> Poison.decode!() + |> Jason.decode!() end def get_token do @@ -38,7 +38,7 @@ def get_token do end def make_auth_body(username, password, tenant) do - Poison.encode!(%{ + Jason.encode!(%{ :auth => %{ :passwordCredentials => %{ :username => username, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index b1adaad2f..c6a562a61 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -105,10 +105,8 @@ def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def user_info(%User{} = user) do - oneself = if user.local, do: 1, else: 0 - %{ - following_count: length(user.following) - oneself, + following_count: following_count(user), note_count: user.info.note_count, follower_count: user.info.follower_count, locked: user.info.locked, @@ -117,6 +115,20 @@ def user_info(%User{} = user) do } end + def restrict_deactivated(query) do + from(u in query, + where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) + ) + end + + def following_count(%User{following: []}), do: 0 + + def following_count(%User{} = user) do + user + |> get_friends_query() + |> Repo.aggregate(:count, :id) + end + def remote_user_creation(params) do params = params @@ -204,14 +216,15 @@ def reset_password(user, data) do end def register_changeset(struct, params \\ %{}, opts \\ []) do - confirmation_status = - if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do - :confirmed + need_confirmation? = + if is_nil(opts[:need_confirmation]) do + Pleroma.Config.get([:instance, :account_activation_required]) else - :unconfirmed + opts[:need_confirmation] end - info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status) + info_change = + User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?) changeset = struct @@ -254,10 +267,7 @@ defp autofollow_users(user) do candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) autofollowed_users = - from(u in User, - where: u.local == true, - where: u.nickname in ^candidates - ) + User.Query.build(%{nickname: candidates, local: true, deactivated: false}) |> Repo.all() follow_all(user, autofollowed_users) @@ -415,24 +425,6 @@ def following?(%User{} = follower, %User{} = followed) do Enum.member?(follower.following, followed.follower_address) end - def follow_import(%User{} = follower, followed_identifiers) - when is_list(followed_identifiers) do - Enum.map( - followed_identifiers, - fn followed_identifier -> - with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), - {:ok, follower} <- maybe_direct_follow(follower, followed), - {:ok, _} <- ActivityPub.follow(follower, followed) do - followed - else - err -> - Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}") - err - end - end - ) - end - def locked?(%User{} = user) do user.info.locked || false end @@ -554,8 +546,7 @@ def get_or_fetch_by_nickname(nickname) do with [_nick, _domain] <- String.split(nickname, "@"), {:ok, user} <- fetch_by_nickname(nickname) do if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do - # TODO turn into job - {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) + fetch_initial_posts(user) end {:ok, user} @@ -566,29 +557,20 @@ def get_or_fetch_by_nickname(nickname) do end @doc "Fetch some posts when the user has just been federated with" - def fetch_initial_posts(user) do - pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) + def fetch_initial_posts(user), + do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user]) - Enum.each( - # Insert all the posts in reverse order, so they're in the right order on the timeline - Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), - &Pleroma.Web.Federator.incoming_ap_doc/1 - ) - end - - def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do - from( - u in User, - where: fragment("? <@ ?", ^[follower_address], u.following), - where: u.id != ^id - ) + @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_followers_query(%User{} = user, nil) do + User.Query.build(%{followers: user, deactivated: false}) end def get_followers_query(user, page) do from(u in get_followers_query(user, nil)) - |> paginate(page, 20) + |> User.Query.paginate(page, 20) end + @spec get_followers_query(User.t()) :: Ecto.Query.t() def get_followers_query(user), do: get_followers_query(user, nil) def get_followers(user, page \\ nil) do @@ -603,19 +585,17 @@ def get_followers_ids(user, page \\ nil) do Repo.all(from(u in q, select: u.id)) end - def get_friends_query(%User{id: id, following: following}, nil) do - from( - u in User, - where: u.follower_address in ^following, - where: u.id != ^id - ) + @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_friends_query(%User{} = user, nil) do + User.Query.build(%{friends: user, deactivated: false}) end def get_friends_query(user, page) do from(u in get_friends_query(user, nil)) - |> paginate(page, 20) + |> User.Query.paginate(page, 20) end + @spec get_friends_query(User.t()) :: Ecto.Query.t() def get_friends_query(user), do: get_friends_query(user, nil) def get_friends(user, page \\ nil) do @@ -630,33 +610,10 @@ def get_friends_ids(user, page \\ nil) do Repo.all(from(u in q, select: u.id)) end - def get_follow_requests_query(%User{} = user) do - from( - a in Activity, - where: - fragment( - "? ->> 'type' = 'Follow'", - a.data - ), - where: - fragment( - "? ->> 'state' = 'pending'", - a.data - ), - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - a.data, - a.data, - ^user.ap_id - ) - ) - end - + @spec get_follow_requests(User.t()) :: {:ok, [User.t()]} def get_follow_requests(%User{} = user) do users = - user - |> User.get_follow_requests_query() + Activity.follow_requests_for_actor(user) |> join(:inner, [a], u in User, on: a.actor == u.ap_id) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) |> group_by([a, u], u.id) @@ -720,18 +677,15 @@ def update_note_count(%User{} = user) do info_cng = User.Info.set_note_count(user.info, note_count) - cng = - change(user) - |> put_embed(:info, info_cng) - - update_and_set_cache(cng) + user + |> change() + |> put_embed(:info, info_cng) + |> update_and_set_cache() end def update_follower_count(%User{} = user) do follower_count_query = - User - |> where([u], ^user.follower_address in u.following) - |> where([u], u.id != ^user.id) + User.Query.build(%{followers: user, deactivated: false}) |> select([u], %{count: count(u.id)}) User @@ -755,38 +709,19 @@ def update_follower_count(%User{} = user) do end end - def get_users_from_set_query(ap_ids, false) do - from( - u in User, - where: u.ap_id in ^ap_ids - ) - end - - def get_users_from_set_query(ap_ids, true) do - query = get_users_from_set_query(ap_ids, false) - - from( - u in query, - where: u.local == true - ) - end - + @spec get_users_from_set([String.t()], boolean()) :: [User.t()] def get_users_from_set(ap_ids, local_only \\ true) do - get_users_from_set_query(ap_ids, local_only) + criteria = %{ap_id: ap_ids, deactivated: false} + criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria + + User.Query.build(criteria) |> Repo.all() end + @spec get_recipients_from_activity(Activity.t()) :: [User.t()] def get_recipients_from_activity(%Activity{recipients: to}) do - query = - from( - u in User, - where: u.ap_id in ^to, - or_where: fragment("? && ?", u.following, ^to) - ) - - query = from(u in query, where: u.local == true) - - Repo.all(query) + User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) + |> Repo.all() end def search(query, resolve \\ false, for_user \\ nil) do @@ -883,6 +818,7 @@ defp fts_search_subquery(term, query \\ User) do ^processed_query ) ) + |> restrict_deactivated() end defp trigram_search_subquery(term) do @@ -901,23 +837,7 @@ defp trigram_search_subquery(term) do }, where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) ) - end - - def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do - Enum.map( - blocked_identifiers, - fn blocked_identifier -> - with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), - {:ok, blocker} <- block(blocker, blocked), - {:ok, _} <- ActivityPub.block(blocker, blocked) do - blocked - else - err -> - Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}") - err - end - end - ) + |> restrict_deactivated() end def mute(muter, %User{ap_id: ap_id}) do @@ -1048,14 +968,23 @@ def subscribed_to?(user, %{ap_id: ap_id}) do end end - def muted_users(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes)) + @spec muted_users(User.t()) :: [User.t()] + def muted_users(user) do + User.Query.build(%{ap_id: user.info.mutes, deactivated: false}) + |> Repo.all() + end - def blocked_users(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks)) + @spec blocked_users(User.t()) :: [User.t()] + def blocked_users(user) do + User.Query.build(%{ap_id: user.info.blocks, deactivated: false}) + |> Repo.all() + end - def subscribers(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers)) + @spec subscribers(User.t()) :: [User.t()] + def subscribers(user) do + User.Query.build(%{ap_id: user.info.subscribers, deactivated: false}) + |> Repo.all() + end def block_domain(user, domain) do info_cng = @@ -1081,77 +1010,25 @@ def unblock_domain(user, domain) do update_and_set_cache(cng) end - def maybe_local_user_query(query, local) do - if local, do: local_user_query(query), else: query - end - - def local_user_query(query \\ User) do - from( - u in query, - where: u.local == true, - where: not is_nil(u.nickname) - ) - end - - def maybe_external_user_query(query, external) do - if external, do: external_user_query(query), else: query - end - - def external_user_query(query \\ User) do - from( - u in query, - where: u.local == false, - where: not is_nil(u.nickname) - ) - end - - def maybe_active_user_query(query, active) do - if active, do: active_user_query(query), else: query - end - - def active_user_query(query \\ User) do - from( - u in query, - where: fragment("not (?->'deactivated' @> 'true')", u.info), - where: not is_nil(u.nickname) - ) - end - - def maybe_deactivated_user_query(query, deactivated) do - if deactivated, do: deactivated_user_query(query), else: query - end - - def deactivated_user_query(query \\ User) do - from( - u in query, - where: fragment("(?->'deactivated' @> 'true')", u.info), - where: not is_nil(u.nickname) - ) - end - - def active_local_user_query do - from( - u in local_user_query(), - where: fragment("not (?->'deactivated' @> 'true')", u.info) - ) - end - - def moderator_user_query do - from( - u in User, - where: u.local == true, - where: fragment("?->'is_moderator' @> 'true'", u.info) - ) + def deactivate_async(user, status \\ true) do + PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status]) end def deactivate(%User{} = user, status \\ true) do info_cng = User.Info.set_activation_status(user.info, status) - cng = - change(user) - |> put_embed(:info, info_cng) + with {:ok, friends} <- User.get_friends(user), + {:ok, followers} <- User.get_followers(user), + {:ok, user} <- + user + |> change() + |> put_embed(:info, info_cng) + |> update_and_set_cache() do + Enum.each(followers, &invalidate_cache(&1)) + Enum.each(friends, &update_follower_count(&1)) - update_and_set_cache(cng) + {:ok, user} + end end def update_notification_settings(%User{} = user, settings \\ %{}) do @@ -1182,6 +1059,75 @@ def perform(:delete, %User{} = user) do delete_user_activities(user) end + @spec perform(atom(), User.t()) :: {:ok, User.t()} + def perform(:fetch_initial_posts, %User{} = user) do + pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) + + Enum.each( + # Insert all the posts in reverse order, so they're in the right order on the timeline + Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), + &Pleroma.Web.Federator.incoming_ap_doc/1 + ) + + {:ok, user} + end + + def perform(:deactivate_async, user, status), do: deactivate(user, status) + + @spec perform(atom(), User.t(), list()) :: list() | {:error, any()} + def perform(:blocks_import, %User{} = blocker, blocked_identifiers) + when is_list(blocked_identifiers) do + Enum.map( + blocked_identifiers, + fn blocked_identifier -> + with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), + {:ok, blocker} <- block(blocker, blocked), + {:ok, _} <- ActivityPub.block(blocker, blocked) do + blocked + else + err -> + Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}") + err + end + end + ) + end + + @spec perform(atom(), User.t(), list()) :: list() | {:error, any()} + def perform(:follow_import, %User{} = follower, followed_identifiers) + when is_list(followed_identifiers) do + Enum.map( + followed_identifiers, + fn followed_identifier -> + with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), + {:ok, follower} <- maybe_direct_follow(follower, followed), + {:ok, _} <- ActivityPub.follow(follower, followed) do + followed + else + err -> + Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}") + err + end + end + ) + end + + def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers), + do: + PleromaJobQueue.enqueue(:background, __MODULE__, [ + :blocks_import, + blocker, + blocked_identifiers + ]) + + def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers), + do: + PleromaJobQueue.enqueue(:background, __MODULE__, [ + :follow_import, + follower, + followed_identifiers + ]) + def delete_user_activities(%User{ap_id: ap_id} = user) do stream = ap_id @@ -1235,8 +1181,8 @@ def get_or_fetch_by_ap_id(ap_id) do resp = fetch_by_ap_id(ap_id) if should_fetch_initial do - with {:ok, %User{} = user} = resp do - {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) + with {:ok, %User{} = user} <- resp do + fetch_initial_posts(user) end end @@ -1306,7 +1252,7 @@ def ap_enabled?(%User{info: info}), do: info.ap_enabled def ap_enabled?(_), do: false @doc "Gets or fetch a user by uri or nickname." - @spec get_or_fetch(String.t()) :: User.t() + @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()} def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri) def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname) @@ -1423,22 +1369,12 @@ def error_user(ap_id) do } end + @spec all_superusers() :: [User.t()] def all_superusers do - from( - u in User, - where: u.local == true, - where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) - ) + User.Query.build(%{super_users: true, local: true, deactivated: false}) |> Repo.all() end - defp paginate(query, page, page_size) do - from(u in query, - limit: ^page_size, - offset: ^((page - 1) * page_size) - ) - end - def showing_reblogs?(%User{} = user, %User{} = target) do target.ap_id not in user.info.muted_reblogs end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 1b81619ce..5a50ee639 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do alias Pleroma.User.Info + @type t :: %__MODULE__{} + embedded_schema do field(:banner, :map, default: %{}) field(:background, :map, default: %{}) @@ -210,21 +212,23 @@ def profile_update(info, params) do ]) end - def confirmation_changeset(info, :confirmed) do - confirmation_changeset(info, %{ - confirmation_pending: false, - confirmation_token: nil - }) - end + @spec confirmation_changeset(Info.t(), keyword()) :: Ecto.Changerset.t() + def confirmation_changeset(info, opts) do + need_confirmation? = Keyword.get(opts, :need_confirmation) - def confirmation_changeset(info, :unconfirmed) do - confirmation_changeset(info, %{ - confirmation_pending: true, - confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() - }) - end + params = + if need_confirmation? do + %{ + confirmation_pending: true, + confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() + } + else + %{ + confirmation_pending: false, + confirmation_token: nil + } + end - def confirmation_changeset(info, params) do cast(info, params, [:confirmation_pending, :confirmation_token]) end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex new file mode 100644 index 000000000..ace9c05f2 --- /dev/null +++ b/lib/pleroma/user/query.ex @@ -0,0 +1,154 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.Query do + @moduledoc """ + User query builder module. Builds query from new query or another user query. + + ## Example: + query = Pleroma.User.Query(%{nickname: "nickname"}) + another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"}) + Pleroma.Repo.all(query) + Pleroma.Repo.all(another_query) + + Adding new rules: + - *ilike criteria* + - add field to @ilike_criteria list + - pass non empty string + - e.g. Pleroma.User.Query.build(%{nickname: "nickname"}) + - *equal criteria* + - add field to @equal_criteria list + - pass non empty string + - e.g. Pleroma.User.Query.build(%{email: "email@example.com"}) + - *contains criteria* + - add field to @containns_criteria list + - pass values list + - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]}) + """ + import Ecto.Query + import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1] + alias Pleroma.User + + @type criteria :: + %{ + query: String.t(), + tags: [String.t()], + name: String.t(), + email: String.t(), + local: boolean(), + external: boolean(), + active: boolean(), + deactivated: boolean(), + is_admin: boolean(), + is_moderator: boolean(), + super_users: boolean(), + followers: User.t(), + friends: User.t(), + recipients_from_activity: [String.t()], + nickname: [String.t()], + ap_id: [String.t()] + } + | %{} + + @ilike_criteria [:nickname, :name, :query] + @equal_criteria [:email] + @role_criteria [:is_admin, :is_moderator] + @contains_criteria [:ap_id, :nickname] + + @spec build(criteria()) :: Query.t() + def build(query \\ base_query(), criteria) do + prepare_query(query, criteria) + end + + @spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t() + def paginate(query, page, page_size) do + from(u in query, + limit: ^page_size, + offset: ^((page - 1) * page_size) + ) + end + + defp base_query do + from(u in User) + end + + defp prepare_query(query, criteria) do + Enum.reduce(criteria, query, &compose_query/2) + end + + defp compose_query({key, value}, query) + when key in @ilike_criteria and not_empty_string(value) do + # hack for :query key + key = if key == :query, do: :nickname, else: key + where(query, [u], ilike(field(u, ^key), ^"%#{value}%")) + end + + defp compose_query({key, value}, query) + when key in @equal_criteria and not_empty_string(value) do + where(query, [u], ^[{key, value}]) + end + + defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do + where(query, [u], field(u, ^key) in ^values) + end + + defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do + Enum.reduce(tags, query, &prepare_tag_criteria/2) + end + + defp compose_query({key, _}, query) when key in @role_criteria do + where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key))) + end + + defp compose_query({:super_users, _}, query) do + where( + query, + [u], + fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) + ) + end + + defp compose_query({:local, _}, query), do: location_query(query, true) + + defp compose_query({:external, _}, query), do: location_query(query, false) + + defp compose_query({:active, _}, query) do + where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info)) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:deactivated, false}, query) do + User.restrict_deactivated(query) + end + + defp compose_query({:deactivated, true}, query) do + where(query, [u], fragment("?->'deactivated' @> 'true'", u.info)) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do + where(query, [u], fragment("? <@ ?", ^[follower_address], u.following)) + |> where([u], u.id != ^id) + end + + defp compose_query({:friends, %User{id: id, following: following}}, query) do + where(query, [u], u.follower_address in ^following) + |> where([u], u.id != ^id) + end + + defp compose_query({:recipients_from_activity, to}, query) do + where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to)) + end + + defp compose_query(_unsupported_param, query), do: query + + defp prepare_tag_criteria(tag, query) do + or_where(query, [u], fragment("? = any(?)", ^tag, u.tags)) + end + + defp location_query(query, local) do + where(query, [u], u.local == ^local) + |> where([u], not is_nil(u.nickname)) + end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 728761ebd..96cb4209b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -5,7 +5,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity alias Pleroma.Conversation - alias Pleroma.Instances alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Fetcher @@ -15,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.Federator alias Pleroma.Web.WebFinger import Ecto.Query @@ -24,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do require Logger - @httpoison Application.get_env(:pleroma, :httpoison) - # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. defp get_recipients(%{"type" => "Announce"} = data) do @@ -137,9 +133,7 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do activity end - Task.start(fn -> - Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end) + PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity]) Notification.create_notifications(activity) @@ -848,6 +842,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_reblogs(opts) |> restrict_pinned(opts) |> restrict_muted_reblogs(opts) + |> Activity.restrict_deactivated_users() end def fetch_activities(recipients, opts \\ %{}) do @@ -951,89 +946,6 @@ def make_user_from_nickname(nickname) do end end - def should_federate?(inbox, public) do - if public do - true - else - inbox_info = URI.parse(inbox) - !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) - end - end - - def publish(actor, activity) do - remote_followers = - if actor.follower_address in activity.recipients do - {:ok, followers} = User.get_followers(actor) - followers |> Enum.filter(&(!&1.local)) - else - [] - end - - public = is_public?(activity) - - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - json = Jason.encode!(data) - - (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) - |> Enum.filter(fn user -> User.ap_enabled?(user) end) - |> Enum.map(fn %{info: %{source_data: data}} -> - (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] - end) - |> Enum.uniq() - |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) - |> Instances.filter_reachable() - |> Enum.each(fn {inbox, unreachable_since} -> - Federator.publish_single_ap(%{ - inbox: inbox, - json: json, - actor: actor, - id: activity.data["id"], - unreachable_since: unreachable_since - }) - end) - end - - def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do - Logger.info("Federating #{id} to #{inbox}") - host = URI.parse(inbox).host - - digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) - - date = - NaiveDateTime.utc_now() - |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") - - signature = - Pleroma.Web.HTTPSignatures.sign(actor, %{ - host: host, - "content-length": byte_size(json), - digest: digest, - date: date - }) - - with {:ok, %{status: code}} when code in 200..299 <- - result = - @httpoison.post( - inbox, - json, - [ - {"Content-Type", "application/activity+json"}, - {"Date", date}, - {"signature", signature}, - {"digest", digest} - ] - ) do - if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], - do: Instances.set_reachable(inbox) - - result - else - {_post_result, response} -> - unless params[:unreachable_since], do: Instances.set_unreachable(inbox) - {:error, response} - end - end - # filter out broken threads def contain_broken_threads(%Activity{} = activity, %User{} = user) do entire_thread_visible_for_user?(activity, user) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex new file mode 100644 index 000000000..11dba87de --- /dev/null +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Publisher do + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Instances + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Web.ActivityPub.Visibility + + @behaviour Pleroma.Web.Federator.Publisher + + require Logger + + @httpoison Application.get_env(:pleroma, :httpoison) + + @moduledoc """ + ActivityPub outgoing federation module. + """ + + @doc """ + Determine if an activity can be represented by running it through Transmogrifier. + """ + def is_representable?(%Activity{} = activity) do + with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do + true + else + _e -> + false + end + end + + @doc """ + Publish a single message to a peer. Takes a struct with the following + parameters set: + + * `inbox`: the inbox to publish to + * `json`: the JSON message body representing the ActivityPub message + * `actor`: the actor which is signing the message + * `id`: the ActivityStreams URI of the message + """ + def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do + Logger.info("Federating #{id} to #{inbox}") + host = URI.parse(inbox).host + + digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) + + date = + NaiveDateTime.utc_now() + |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + + signature = + Pleroma.Signature.sign(actor, %{ + host: host, + "content-length": byte_size(json), + digest: digest, + date: date + }) + + with {:ok, %{status: code}} when code in 200..299 <- + result = + @httpoison.post( + inbox, + json, + [ + {"Content-Type", "application/activity+json"}, + {"Date", date}, + {"signature", signature}, + {"digest", digest} + ] + ) do + if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], + do: Instances.set_reachable(inbox) + + result + else + {_post_result, response} -> + unless params[:unreachable_since], do: Instances.set_unreachable(inbox) + {:error, response} + end + end + + defp should_federate?(inbox, public) do + if public do + true + else + inbox_info = URI.parse(inbox) + !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) + end + end + + @doc """ + Publishes an activity to all relevant peers. + """ + def publish(%User{} = actor, %Activity{} = activity) do + remote_followers = + if actor.follower_address in activity.recipients do + {:ok, followers} = User.get_followers(actor) + followers |> Enum.filter(&(!&1.local)) + else + [] + end + + public = is_public?(activity) + + if public && Config.get([:instance, :allow_relay]) do + Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) + Relay.publish(activity) + end + + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + json = Jason.encode!(data) + + (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) + |> Enum.filter(fn user -> User.ap_enabled?(user) end) + |> Enum.map(fn %{info: %{source_data: data}} -> + (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] + end) + |> Enum.uniq() + |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) + |> Instances.filter_reachable() + |> Enum.each(fn {inbox, unreachable_since} -> + Pleroma.Web.Federator.Publisher.enqueue_one( + __MODULE__, + %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"], + unreachable_since: unreachable_since + } + ) + end) + end + + def gather_webfinger_links(%User{} = user) do + [ + %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id}, + %{ + "rel" => "self", + "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "href" => user.ap_id + } + ] + end + + def gather_nodeinfo_protocol_names, do: ["activitypub"] +end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 581b9d1ab..236d1b4ac 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -682,7 +682,7 @@ def make_flag_data(params, additional) do """ def fetch_ordered_collection(from, pages_left, acc \\ []) do with {:ok, response} <- Tesla.get(from), - {:ok, collection} <- Poison.decode(response.body) do + {:ok, collection} <- Jason.decode(response.body) do case collection["type"] do "OrderedCollection" -> # If we've encountered the OrderedCollection and not the page, diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index e7613a5c8..c1b55ac6f 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -59,4 +59,28 @@ def entire_thread_visible_for_user?( visible_for_user?(tail, user) 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 end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 711f233a6..e00b33aba 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -59,7 +59,7 @@ def user_create( bio: "." } - changeset = User.register_changeset(%User{}, user_data, confirmed: true) + changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) {:ok, user} = User.register(changeset) conn @@ -101,7 +101,10 @@ def list_users(conn, params) do search_params = %{ query: params["query"], page: page, - page_size: page_size + page_size: page_size, + tags: params["tags"], + name: params["name"], + email: params["email"] } with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), @@ -116,11 +119,11 @@ def list_users(conn, params) do ) end - @filters ~w(local external active deactivated) - - defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} + @filters ~w(local external active deactivated is_admin is_moderator) @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} + defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} + defp maybe_parse_filters(filters) do filters |> String.split(",") diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index 9a8e41c2a..ed919833e 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -10,45 +10,23 @@ defmodule Pleroma.Web.AdminAPI.Search do @page_size 50 - def user(%{query: term} = params) when is_nil(term) or term == "" do - query = maybe_filtered_query(params) + defmacro not_empty_string(string) do + quote do + is_binary(unquote(string)) and unquote(string) != "" + end + end + + @spec user(map()) :: {:ok, [User.t()], pos_integer()} + def user(params \\ %{}) do + query = User.Query.build(params) |> order_by([u], u.nickname) paginated_query = - maybe_filtered_query(params) - |> paginate(params[:page] || 1, params[:page_size] || @page_size) + User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size) - count = query |> Repo.aggregate(:count, :id) + count = Repo.aggregate(query, :count, :id) results = Repo.all(paginated_query) {:ok, results, count} end - - def user(%{query: term} = params) when is_binary(term) do - search_query = from(u in maybe_filtered_query(params), where: ilike(u.nickname, ^"%#{term}%")) - - count = search_query |> Repo.aggregate(:count, :id) - - results = - search_query - |> paginate(params[:page] || 1, params[:page_size] || @page_size) - |> Repo.all() - - {:ok, results, count} - end - - defp maybe_filtered_query(params) do - from(u in User, order_by: u.nickname) - |> User.maybe_local_user_query(params[:local]) - |> User.maybe_external_user_query(params[:external]) - |> User.maybe_active_user_query(params[:active]) - |> User.maybe_deactivated_user_query(params[:deactivated]) - end - - defp paginate(query, page, page_size) do - from(u in query, - limit: ^page_size, - offset: ^((page - 1) * page_size) - ) - end end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index dd79cdcf7..c4a6fce08 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -74,7 +74,7 @@ def create_from_registration( password_confirmation: random_password }, external: true, - confirmed: true + need_confirmation: false ) |> Repo.insert(), {:ok, _} <- diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index b53869c75..29c4c1014 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -116,32 +116,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 +187,8 @@ def post(user, %{"status" => status} = data) do ) res + else + e -> {:error, e} end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 7f939991d..9ef30e885 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -29,6 +29,13 @@ defmodule Pleroma.Web.Endpoint do # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength ) + plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") + + plug(Plug.Static, + at: "/pleroma/admin/", + from: {:pleroma, "priv/static/adminfe/"} + ) + # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 29e178ba9..169fdf4dc 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -7,13 +7,10 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Object.Containment alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Federator.RetryQueue - alias Pleroma.Web.OStatus - alias Pleroma.Web.Salmon alias Pleroma.Web.WebFinger alias Pleroma.Web.Websub @@ -42,14 +39,6 @@ def publish(activity, priority \\ 1) do PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority) end - def publish_single_ap(params) do - PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params]) - end - - def publish_single_websub(websub) do - PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub]) - end - def verify_websub(websub) do PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub]) end @@ -62,10 +51,6 @@ def refresh_subscriptions do PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions]) end - def publish_single_salmon(params) do - PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params]) - end - # Job Worker Callbacks def perform(:refresh_subscriptions) do @@ -95,23 +80,7 @@ def perform(:publish, activity) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do {:ok, actor} = WebFinger.ensure_keys_present(actor) - if Visibility.is_public?(activity) do - if OStatus.is_representable?(activity) do - Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) - Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) - - Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end) - Pleroma.Web.Salmon.publish(actor, activity) - end - - if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do - Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) - Relay.publish(activity) - end - end - - Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) - Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity) + Publisher.publish(actor, activity) end end @@ -148,25 +117,11 @@ def perform(:incoming_ap_doc, params) do _e -> # Just drop those for now Logger.info("Unhandled activity") - Logger.info(Poison.encode!(params, pretty: 2)) + Logger.info(Jason.encode!(params, pretty: true)) :error end end - def perform(:publish_single_salmon, params) do - Salmon.send_to_user(params) - end - - def perform(:publish_single_ap, params) do - case ActivityPub.publish_one(params) do - {:ok, _} -> - :ok - - {:error, _} -> - RetryQueue.enqueue(params, ActivityPub) - end - end - def perform( :publish_single_websub, %{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex new file mode 100644 index 000000000..916bcdcba --- /dev/null +++ b/lib/pleroma/web/federator/publisher.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Federator.Publisher do + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.Federator.RetryQueue + + require Logger + + @moduledoc """ + Defines the contract used by federation implementations to publish messages to + their peers. + """ + + @doc """ + Determine whether an activity can be relayed using the federation module. + """ + @callback is_representable?(Pleroma.Activity.t()) :: boolean() + + @doc """ + Relays an activity to a specified peer, determined by the parameters. The + parameters used are controlled by the federation module. + """ + @callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()} + + @doc """ + Enqueue publishing a single activity. + """ + @spec enqueue_one(module(), Map.t()) :: :ok + def enqueue_one(module, %{} = params), + do: PleromaJobQueue.enqueue(:federation_outgoing, __MODULE__, [:publish_one, module, params]) + + @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} + def perform(:publish_one, module, params) do + case apply(module, :publish_one, [params]) do + {:ok, _} -> + :ok + + {:error, _e} -> + RetryQueue.enqueue(params, module) + end + end + + def perform(type, _, _) do + Logger.debug("Unknown task: #{type}") + {:error, "Don't know what to do with this"} + end + + @doc """ + Relays an activity to all specified peers. + """ + @callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()} + + @spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok + def publish(%User{} = user, %Activity{} = activity) do + Config.get([:instance, :federation_publisher_modules]) + |> Enum.each(fn module -> + if module.is_representable?(activity) do + Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}") + module.publish(user, activity) + end + end) + + :ok + end + + @doc """ + Gathers links used by an outgoing federation module for WebFinger output. + """ + @callback gather_webfinger_links(Pleroma.User.t()) :: list() + + @spec gather_webfinger_links(Pleroma.User.t()) :: list() + def gather_webfinger_links(%User{} = user) do + Config.get([:instance, :federation_publisher_modules]) + |> Enum.reduce([], fn module, links -> + links ++ module.gather_webfinger_links(user) + end) + end + + @doc """ + Gathers nodeinfo protocol names supported by the federation module. + """ + @callback gather_nodeinfo_protocol_names() :: list() + + @spec gather_nodeinfo_protocol_names() :: list() + def gather_nodeinfo_protocol_names do + Config.get([:instance, :federation_publisher_modules]) + |> Enum.reduce([], fn module, links -> + links ++ module.gather_nodeinfo_protocol_names() + end) + end +end 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 956736780..87e597074 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -39,12 +39,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.ControllerHelper import Ecto.Query require Logger + plug( + Pleroma.Plugs.RateLimitPlug, + %{ + max_requests: Config.get([:app_account_creation, :max_requests]), + interval: Config.get([:app_account_creation, :interval]) + } + when action in [:account_register] + ) + @httpoison Application.get_env(:pleroma, :httpoison) @local_mastodon_name "Mastodon-Local" @@ -168,7 +178,7 @@ def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do end end - @mastodon_api_level "2.6.5" + @mastodon_api_level "2.7.2" def masto_instance(conn, _params) do instance = Config.get(:instance) @@ -1536,7 +1546,7 @@ def create_filter( user_id: user.id, phrase: phrase, context: context, - hide: Map.get(params, "irreversible", nil), + hide: Map.get(params, "irreversible", false), whole_word: Map.get(params, "boolean", true) # expires_at } @@ -1693,6 +1703,53 @@ def reports(%{assigns: %{user: user}} = conn, params) do end end + def account_register( + %{assigns: %{app: app}} = conn, + %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params + ) do + params = + params + |> Map.take([ + "email", + "captcha_solution", + "captcha_token", + "captcha_answer_data", + "token", + "password" + ]) + |> Map.put("nickname", nickname) + |> Map.put("fullname", params["fullname"] || nickname) + |> Map.put("bio", params["bio"] || "") + |> Map.put("confirm", params["password"]) + + with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), + {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do + json(conn, %{ + token_type: "Bearer", + access_token: token.token, + scope: app.scopes, + created_at: Token.Utils.format_created_at(token) + }) + else + {:error, errors} -> + conn + |> put_status(400) + |> json(Jason.encode!(errors)) + end + end + + def account_register(%{assigns: %{app: _app}} = conn, _params) do + conn + |> put_status(400) + |> json(%{error: "Missing parameters"}) + end + + def account_register(conn, _) do + conn + |> put_status(403) + |> json(%{error: "Invalid credentials"}) + end + def conversations(%{assigns: %{user: user}} = conn, params) do participations = Participation.for_user_with_last_activity_id(user, params) 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/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 216a962bd..3bf2a0fbc 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.MRF + alias Pleroma.Web.Federator.Publisher plug(Pleroma.Web.FederatingPlug) @@ -137,7 +138,7 @@ def raw_nodeinfo do name: Pleroma.Application.name() |> String.downcase(), version: Pleroma.Application.version() }, - protocols: ["ostatus", "activitypub"], + protocols: Publisher.gather_nodeinfo_protocol_names(), services: %{ inbound: [], outbound: [] diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index bccc2ac96..ddcdb1871 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do import Ecto.Changeset @type t :: %__MODULE__{} + schema "apps" do field(:client_name, :string) field(:redirect_uris, :string) diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index ca3901cc4..b47688de1 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do import Ecto.Query @type t :: %__MODULE__{} + schema "oauth_authorizations" do field(:token, :string) field(:scopes, {:array, :string}, default: []) @@ -25,28 +26,45 @@ defmodule Pleroma.Web.OAuth.Authorization do timestamps() end + @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do - scopes = scopes || app.scopes - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - - authorization = %Authorization{ - token: token, - used: false, + %{ + scopes: scopes || app.scopes, user_id: user.id, - app_id: app.id, - scopes: scopes, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) + app_id: app.id } - - Repo.insert(authorization) + |> create_changeset() + |> Repo.insert() end + @spec create_changeset(map()) :: Changeset.t() + def create_changeset(attrs \\ %{}) do + %Authorization{} + |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) + |> validate_required([:app_id, :scopes]) + |> add_token() + |> add_lifetime() + end + + defp add_token(changeset) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + put_change(changeset, :token, token) + end + + defp add_lifetime(changeset) do + put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) + end + + @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() def use_changeset(%Authorization{} = auth, params) do auth |> cast(params, [:used]) |> validate_required([:used]) end + @spec use_token(Authorization.t()) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do Repo.update(use_changeset(auth, %{used: true})) @@ -57,6 +75,7 @@ def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do def use_token(%Authorization{used: true}), do: {:error, "already used"} + @spec delete_user_authorizations(User.t()) :: {integer(), any()} def delete_user_authorizations(%User{id: user_id}) do from( a in Pleroma.Web.OAuth.Authorization, diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 8ee0da667..ae2b80d95 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -19,8 +19,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) - plug(:fetch_session) plug(:fetch_flash) @@ -144,14 +142,14 @@ defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do @doc "Renew access_token with refresh_token" def token_exchange( conn, - %{"grant_type" => "refresh_token", "refresh_token" => token} = params + %{"grant_type" => "refresh_token", "refresh_token" => token} = _params ) do - with %App{} = app <- get_app_from_request(conn, params), + with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, token} <- RefreshToken.grant(token) do response_attrs = %{created_at: Token.Utils.format_created_at(token)} - json(conn, response_token(user, token, response_attrs)) + json(conn, Token.Response.build(user, token, response_attrs)) else _error -> put_status(conn, 400) @@ -160,14 +158,14 @@ def token_exchange( end def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do - with %App{} = app <- get_app_from_request(conn, params), + with {:ok, app} <- Token.Utils.fetch_app(conn), fixed_token = Token.Utils.fix_padding(params["code"]), {:ok, auth} <- Authorization.get_by_token(app, fixed_token), %User{} = user <- User.get_cached_by_id(auth.user_id), {:ok, token} <- Token.exchange_token(app, auth) do response_attrs = %{created_at: Token.Utils.format_created_at(token)} - json(conn, response_token(user, token, response_attrs)) + json(conn, Token.Response.build(user, token, response_attrs)) else _error -> put_status(conn, 400) @@ -179,14 +177,14 @@ def token_exchange( conn, %{"grant_type" => "password"} = params ) do - with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, - %App{} = app <- get_app_from_request(conn, params), + with {:ok, %User{} = user} <- Authenticator.get_user(conn), + {:ok, app} <- Token.Utils.fetch_app(conn), {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:user_active, true} <- {:user_active, !user.info.deactivated}, {:ok, scopes} <- validate_scopes(app, params), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, response_token(user, token)) + json(conn, Token.Response.build(user, token)) else {:auth_active, false} -> # Per https://github.com/tootsuite/mastodon/blob/ @@ -218,11 +216,23 @@ def token_exchange( token_exchange(conn, params) end + def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do + with {:ok, app} <- Token.Utils.fetch_app(conn), + {:ok, auth} <- Authorization.create_authorization(app, %User{}), + {:ok, token} <- Token.exchange_token(app, auth) do + json(conn, Token.Response.build_for_client_credentials(token)) + else + _error -> + put_status(conn, 400) + |> json(%{error: "Invalid credentials"}) + end + end + # Bad request def token_exchange(conn, params), do: bad_request(conn, params) def token_revoke(conn, %{"token" => _token} = params) do - with %App{} = app <- get_app_from_request(conn, params), + with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, _token} <- RevokeToken.revoke(app, params) do json(conn, %{}) else @@ -252,7 +262,7 @@ def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attr auth_attrs |> Map.delete("scopes") |> Map.put("scope", scope) - |> Poison.encode!() + |> Jason.encode!() params = auth_attrs @@ -316,7 +326,7 @@ def callback(conn, params) do end defp callback_params(%{"state" => state} = params) do - Map.merge(params, Poison.decode!(state)) + Map.merge(params, Jason.decode!(state)) end def registration_details(conn, %{"authorization" => auth_attrs}) do @@ -405,33 +415,6 @@ defp do_create_authorization( end end - defp get_app_from_request(conn, params) do - conn - |> fetch_client_credentials(params) - |> fetch_client - end - - defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do - Repo.get_by(App, client_id: id, client_secret: secret) - end - - defp fetch_client({_id, _secret}), do: nil - - defp fetch_client_credentials(conn, params) do - # Per RFC 6749, HTTP Basic is preferred to body params - with ["Basic " <> encoded] <- get_req_header(conn, "authorization"), - {:ok, decoded} <- Base.decode64(encoded), - [id, secret] <- - Enum.map( - String.split(decoded, ":"), - fn s -> URI.decode_www_form(s) end - ) do - {id, secret} - else - _ -> {params["client_id"], params["client_secret"]} - end - end - # Special case: Local MastodonFE defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) @@ -442,18 +425,6 @@ defp get_session_registration_id(conn), do: get_session(conn, :registration_id) defp put_session_registration_id(conn, registration_id), do: put_session(conn, :registration_id, registration_id) - defp response_token(%User{} = user, token, opts \\ %{}) do - %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: @expires_in, - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } - |> Map.merge(opts) - end - @spec validate_scopes(App.t(), map()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} defp validate_scopes(app, params) do diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 4e5d1d118..ef047d565 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -45,12 +45,16 @@ def get_by_refresh_token(%App{id: app_id} = _app, token) do |> Repo.find_resource() end + @spec exchange_token(App.t(), Authorization.t()) :: + {:ok, Token.t()} | {:error, Changeset.t()} def exchange_token(app, auth) do with {:ok, auth} <- Authorization.use_token(auth), true <- auth.app_id == app.id do + user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} + create_token( app, - User.get_cached_by_id(auth.user_id), + user, %{scopes: auth.scopes} ) end @@ -81,12 +85,13 @@ defp put_valid_until(changeset, attrs) do |> validate_required([:valid_until]) end + @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do %__MODULE__{user_id: user.id, app_id: app.id} |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) - |> validate_required([:scopes, :user_id, :app_id]) + |> validate_required([:scopes, :app_id]) |> put_valid_until(attrs) - |> put_token + |> put_token() |> put_refresh_token(attrs) |> Repo.insert() end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex new file mode 100644 index 000000000..64e78b183 --- /dev/null +++ b/lib/pleroma/web/oauth/token/response.ex @@ -0,0 +1,32 @@ +defmodule Pleroma.Web.OAuth.Token.Response do + @moduledoc false + + alias Pleroma.User + alias Pleroma.Web.OAuth.Token.Utils + + @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) + + @doc false + def build(%User{} = user, token, opts \\ %{}) do + %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: @expires_in, + scope: Enum.join(token.scopes, " "), + me: user.ap_id + } + |> Map.merge(opts) + end + + def build_for_client_credentials(token) do + %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + created_at: Utils.format_created_at(token), + expires_in: @expires_in, + scope: Enum.join(token.scopes, " ") + } + end +end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex index a81560a1c..7a4fddafd 100644 --- a/lib/pleroma/web/oauth/token/utils.ex +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -3,6 +3,44 @@ defmodule Pleroma.Web.OAuth.Token.Utils do Auxiliary functions for dealing with tokens. """ + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + + @doc "Fetch app by client credentials from request" + @spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found} + def fetch_app(conn) do + res = + conn + |> fetch_client_credentials() + |> fetch_client + + case res do + %App{} = app -> {:ok, app} + _ -> {:error, :not_found} + end + end + + defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do + Repo.get_by(App, client_id: id, client_secret: secret) + end + + defp fetch_client({_id, _secret}), do: nil + + defp fetch_client_credentials(conn) do + # Per RFC 6749, HTTP Basic is preferred to body params + with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"), + {:ok, decoded} <- Base.decode64(encoded), + [id, secret] <- + Enum.map( + String.split(decoded, ":"), + fn s -> URI.decode_www_form(s) end + ) do + {id, secret} + else + _ -> {conn.params["client_id"], conn.params["client_secret"]} + end + end + @doc "convert token inserted_at to unix timestamp" def format_created_at(%{inserted_at: inserted_at} = _token) do inserted_at diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 166691a09..95037125d 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -18,15 +18,18 @@ defp get_href(id) do end end - defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do - [ - {:"thr:in-reply-to", - [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} - ] + defp get_in_reply_to(activity) do + with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do + [ + {:"thr:in-reply-to", + [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} + ] + else + _ -> + [] + end end - defp get_in_reply_to(_), do: [] - defp get_mentions(to) do Enum.map(to, fn id -> cond do @@ -98,7 +101,7 @@ def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) []} end) - in_reply_to = get_in_reply_to(activity.data) + in_reply_to = get_in_reply_to(activity) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = activity.recipients |> get_mentions @@ -146,7 +149,6 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d updated_at = activity.data["published"] inserted_at = activity.data["published"] - _in_reply_to = get_in_reply_to(activity.data) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = activity.recipients |> get_mentions @@ -177,7 +179,6 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho updated_at = activity.data["published"] inserted_at = activity.data["published"] - _in_reply_to = get_in_reply_to(activity.data) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 4744c6d83..61515b31e 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.OStatus do alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.OStatus.DeleteHandler alias Pleroma.Web.OStatus.FollowHandler alias Pleroma.Web.OStatus.NoteHandler @@ -30,7 +31,7 @@ def is_representable?(%Activity{} = activity) do is_nil(object) -> false - object.data["type"] == "Note" -> + Visibility.is_public?(activity) && object.data["type"] == "Note" -> true true -> diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index f67aaf58b..0162a5be9 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -34,4 +34,6 @@ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) d end def fetch_data_for_activity(_), do: %{} + + def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity) end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6d9c77c1a..7fef82f82 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 @@ -146,34 +148,52 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through([:admin_api, :oauth_write]) - post("/user/follow", AdminAPIController, :user_follow) - post("/user/unfollow", AdminAPIController, :user_unfollow) - - get("/users", AdminAPIController, :list_users) - get("/users/:nickname", AdminAPIController, :user_show) + post("/users/follow", AdminAPIController, :user_follow) + post("/users/unfollow", AdminAPIController, :user_unfollow) + # TODO: to be removed at version 1.0 delete("/user", AdminAPIController, :user_delete) - patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) post("/user", AdminAPIController, :user_create) + + delete("/users", AdminAPIController, :user_delete) + post("/users", AdminAPIController, :user_create) + patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) + # TODO: to be removed at version 1.0 get("/permission_group/:nickname", AdminAPIController, :right_get) get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get) post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add) delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete) - put("/activation_status/:nickname", AdminAPIController, :set_activation_status) + get("/users/:nickname/permission_group", AdminAPIController, :right_get) + get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get) + post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add) + + delete( + "/users/:nickname/permission_group/:permission_group", + AdminAPIController, + :right_delete + ) + + put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status) post("/relay", AdminAPIController, :relay_follow) delete("/relay", AdminAPIController, :relay_unfollow) - get("/invite_token", AdminAPIController, :get_invite_token) - get("/invites", AdminAPIController, :invites) - post("/revoke_invite", AdminAPIController, :revoke_invite) - post("/email_invite", AdminAPIController, :email_invite) + get("/users/invite_token", AdminAPIController, :get_invite_token) + get("/users/invites", AdminAPIController, :invites) + post("/users/revoke_invite", AdminAPIController, :revoke_invite) + post("/users/email_invite", AdminAPIController, :email_invite) + # TODO: to be removed at version 1.0 get("/password_reset", AdminAPIController, :get_password_reset) + + get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) + + get("/users", AdminAPIController, :list_users) + get("/users/:nickname", AdminAPIController, :user_show) end scope "/", Pleroma.Web.TwitterAPI do @@ -197,6 +217,7 @@ defmodule Pleroma.Web.Router do post("/change_password", UtilController, :change_password) post("/delete_account", UtilController, :delete_account) put("/notification_settings", UtilController, :update_notificaton_settings) + post("/disable_account", UtilController, :disable_account) end scope [] do @@ -367,6 +388,8 @@ defmodule Pleroma.Web.Router do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:api) + post("/accounts", MastodonAPIController, :account_register) + get("/instance", MastodonAPIController, :masto_instance) get("/instance/peers", MastodonAPIController, :peers) post("/apps", MastodonAPIController, :create_app) @@ -383,7 +406,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) @@ -404,7 +427,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 @@ -434,7 +457,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) @@ -452,7 +475,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) @@ -466,7 +489,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 @@ -650,7 +673,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/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 0a9e51656..42709ab47 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -3,12 +3,18 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Salmon do + @behaviour Pleroma.Web.Federator.Publisher + @httpoison Application.get_env(:pleroma, :httpoison) use Bitwise + alias Pleroma.Activity alias Pleroma.Instances alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Federator.Publisher + alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.XML @@ -165,12 +171,12 @@ def remote_users(%{data: %{"to" => to} = data}) do end @doc "Pushes an activity to remote account." - def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params), - do: send_to_user(Map.put(params, :recipient, salmon)) + def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params), + do: publish_one(Map.put(params, :recipient, salmon)) - def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do + def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do with {:ok, %{status: code}} when code in 200..299 <- - poster.( + @httpoison.post( url, feed, [{"Content-Type", "application/magic-envelope+xml"}] @@ -184,11 +190,11 @@ def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is e -> unless params[:unreachable_since], do: Instances.set_reachable(url) Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) - :error + {:error, "Unreachable instance"} end end - def send_to_user(_), do: :noop + def publish_one(_), do: :noop @supported_activities [ "Create", @@ -199,13 +205,19 @@ def send_to_user(_), do: :noop "Delete" ] + def is_representable?(%Activity{data: %{"type" => type}} = activity) + when type in @supported_activities, + do: Visibility.is_public?(activity) + + def is_representable?(_), do: false + @doc """ Publishes an activity to remote accounts """ - @spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none - def publish(user, activity, poster \\ &@httpoison.post/3) + @spec publish(User.t(), Pleroma.Activity.t()) :: none + def publish(user, activity) - def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster) + def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity) when type in @supported_activities do feed = ActivityRepresenter.to_simple_form(activity, user, true) @@ -229,15 +241,29 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity |> Enum.each(fn remote_user -> Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) - Pleroma.Web.Federator.publish_single_salmon(%{ + Publisher.enqueue_one(__MODULE__, %{ recipient: remote_user, feed: feed, - poster: poster, unreachable_since: reachable_urls_metadata[remote_user.info.salmon] }) end) end end - def publish(%{id: id}, _, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) + def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) + + def gather_webfinger_links(%User{} = user) do + {:ok, _private, public} = keys_from_pem(user.info.keys) + magic_key = encode_key(public) + + [ + %{"rel" => "salmon", "href" => OStatus.salmon_path(user)}, + %{ + "rel" => "magic-public-key", + "href" => "data:application/magic-public-key,#{magic_key}" + } + ] + end + + def gather_nodeinfo_protocol_names, do: [] end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index c03f8ab3a..489170d80 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -173,8 +173,6 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ def config(conn, _params) do instance = Pleroma.Config.get(:instance) - instance_fe = Pleroma.Config.get(:fe) - instance_chat = Pleroma.Config.get(:chat) case get_format(conn) do "xml" -> @@ -219,31 +217,7 @@ def config(conn, _params) do if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0") } - pleroma_fe = - if instance_fe do - %{ - theme: Keyword.get(instance_fe, :theme), - background: Keyword.get(instance_fe, :background), - logo: Keyword.get(instance_fe, :logo), - logoMask: Keyword.get(instance_fe, :logo_mask), - logoMargin: Keyword.get(instance_fe, :logo_margin), - redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login), - redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login), - chatDisabled: !Keyword.get(instance_chat, :enabled), - showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel), - scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled), - formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled), - collapseMessageWithSubject: - Keyword.get(instance_fe, :collapse_message_with_subject), - hidePostStats: Keyword.get(instance_fe, :hide_post_stats), - hideUserStats: Keyword.get(instance_fe, :hide_user_stats), - scopeCopy: Keyword.get(instance_fe, :scope_copy), - subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior), - alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input) - } - else - Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) - end + pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) managed_config = Keyword.get(instance, :managed_config) @@ -309,8 +283,13 @@ def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do Enum.map(lines, fn line -> String.split(line, ",") |> List.first() end) - |> List.delete("Account address"), - {:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do + |> List.delete("Account address") do + PleromaJobQueue.enqueue(:background, User, [ + :follow_import, + follower, + followed_identifiers + ]) + json(conn, "job started") end end @@ -320,8 +299,13 @@ def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do end def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do - with blocked_identifiers <- String.split(list), - {:ok, _} = Task.start(fn -> User.blocks_import(blocker, blocked_identifiers) end) do + with blocked_identifiers <- String.split(list) do + PleromaJobQueue.enqueue(:background, User, [ + :blocks_import, + blocker, + blocked_identifiers + ]) + json(conn, "job started") end end @@ -360,6 +344,17 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do end end + def disable_account(%{assigns: %{user: user}} = conn, params) do + case CommonAPI.Utils.confirm_current_password(user, params["password"]) do + {:ok, user} -> + User.deactivate_async(user) + json(conn, %{status: "success"}) + + {:error, msg} -> + json(conn, %{error: msg}) + end + end + def captcha(conn, _params) do json(conn, Pleroma.Captcha.new()) end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 3a7774647..41e1c2877 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -128,7 +128,7 @@ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do end end - def register_user(params) do + def register_user(params, opts \\ []) do token = params["token"] params = %{ @@ -162,13 +162,22 @@ def register_user(params) do # I have no idea how this error handling works {:error, %{error: Jason.encode!(%{captcha: [error]})}} else - registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - registration_process(registrations_open, params, token) + registration_process( + params, + %{ + registrations_open: Pleroma.Config.get([:instance, :registrations_open]), + token: token + }, + opts + ) end end - defp registration_process(registration_open, params, token) - when registration_open == false or is_nil(registration_open) do + defp registration_process(params, %{registrations_open: true}, opts) do + create_user(params, opts) + end + + defp registration_process(params, %{token: token}, opts) do invite = unless is_nil(token) do Repo.get_by(UserInviteToken, %{token: token}) @@ -182,19 +191,15 @@ defp registration_process(registration_open, params, token) invite when valid_invite? -> UserInviteToken.update_usage!(invite) - create_user(params) + create_user(params, opts) _ -> {:error, "Expired token"} end end - defp registration_process(true, params, _token) do - create_user(params) - end - - defp create_user(params) do - changeset = User.register_changeset(%User{}, params) + defp create_user(params, opts) do + changeset = User.register_changeset(%User{}, params, opts) case User.register(changeset) do {:ok, user} -> @@ -231,12 +236,15 @@ def password_reset(nickname_or_email) do def get_user(user \\ nil, params) do case params do %{"user_id" => user_id} -> - case target = User.get_cached_by_nickname_or_id(user_id) do + case User.get_cached_by_nickname_or_id(user_id) do nil -> {:error, "No user with such user_id"} - _ -> - {:ok, target} + %User{info: %{deactivated: true}} -> + {:error, "User has been disabled"} + + user -> + {:ok, user} end %{"screen_name" => nickname} -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 21e6c555a..3c5a70be9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -440,7 +440,7 @@ def confirm_email(conn, %{"user_id" => uid, "token" => token}) do true <- user.local, true <- user.info.confirmation_pending, true <- user.info.confirmation_token == token, - info_change <- User.Info.confirmation_changeset(user.info, :confirmed), + info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change), {:ok, _} <- User.update_and_set_cache(changeset) do conn diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index c64152da8..44bcafe0e 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -170,7 +170,7 @@ def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activ created_at = activity.data["published"] |> Utils.date_to_asctime() announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) - text = "#{user.nickname} retweeted a status." + text = "#{user.nickname} repeated a status." retweeted_status = render("activity.json", Map.merge(opts, %{activity: announced_activity})) @@ -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 a3b0bf999..3a3b98a10 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.WebFinger do alias Pleroma.User alias Pleroma.Web - alias Pleroma.Web.OStatus + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Salmon alias Pleroma.Web.XML alias Pleroma.XmlBuilder @@ -50,70 +50,40 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do end end + defp gather_links(%User{} = user) do + [ + %{ + "rel" => "http://webfinger.net/rel/profile-page", + "type" => "text/html", + "href" => user.ap_id + } + ] ++ Publisher.gather_webfinger_links(user) + end + def represent_user(user, "JSON") do {:ok, user} = ensure_keys_present(user) - {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys) - magic_key = Salmon.encode_key(public) %{ "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "aliases" => [user.ap_id], - "links" => [ - %{ - "rel" => "http://schemas.google.com/g/2010#updates-from", - "type" => "application/atom+xml", - "href" => OStatus.feed_path(user) - }, - %{ - "rel" => "http://webfinger.net/rel/profile-page", - "type" => "text/html", - "href" => user.ap_id - }, - %{"rel" => "salmon", "href" => OStatus.salmon_path(user)}, - %{ - "rel" => "magic-public-key", - "href" => "data:application/magic-public-key,#{magic_key}" - }, - %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id}, - %{ - "rel" => "self", - "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", - "href" => user.ap_id - }, - %{ - "rel" => "http://ostatus.org/schema/1.0/subscribe", - "template" => OStatus.remote_follow_path() - } - ] + "links" => gather_links(user) } end def represent_user(user, "XML") do {:ok, user} = ensure_keys_present(user) - {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys) - magic_key = Salmon.encode_key(public) + + links = + gather_links(user) + |> Enum.map(fn link -> {:Link, link} end) { :XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, [ {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, - {:Alias, user.ap_id}, - {:Link, - %{ - rel: "http://schemas.google.com/g/2010#updates-from", - type: "application/atom+xml", - href: OStatus.feed_path(user) - }}, - {:Link, - %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}}, - {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}}, - {:Link, - %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}}, - {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}}, - {:Link, - %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}} - ] + {:Alias, user.ap_id} + ] ++ links } |> XmlBuilder.to_doc() end diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex index 3ffa6b416..7ad0414ab 100644 --- a/lib/pleroma/web/websub/websub.ex +++ b/lib/pleroma/web/websub/websub.ex @@ -4,10 +4,14 @@ defmodule Pleroma.Web.Websub do alias Ecto.Changeset + alias Pleroma.Activity alias Pleroma.Instances alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Endpoint alias Pleroma.Web.Federator + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus.FeedRepresenter alias Pleroma.Web.Router.Helpers @@ -18,6 +22,8 @@ defmodule Pleroma.Web.Websub do import Ecto.Query + @behaviour Pleroma.Web.Federator.Publisher + @httpoison Application.get_env(:pleroma, :httpoison) def verify(subscription, getter \\ &@httpoison.get/3) do @@ -56,6 +62,13 @@ def verify(subscription, getter \\ &@httpoison.get/3) do "Undo", "Delete" ] + + def is_representable?(%Activity{data: %{"type" => type}} = activity) + when type in @supported_activities, + do: Visibility.is_public?(activity) + + def is_representable?(_), do: false + def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @supported_activities do response = @@ -88,12 +101,14 @@ def publish(topic, user, %{data: %{"type" => type}} = activity) unreachable_since: reachable_callbacks_metadata[sub.callback] } - Federator.publish_single_websub(data) + Publisher.enqueue_one(__MODULE__, data) end) end def publish(_, _, _), do: "" + def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) + def sign(secret, doc) do :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase() end @@ -299,4 +314,20 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = {:error, response} end end + + def gather_webfinger_links(%User{} = user) do + [ + %{ + "rel" => "http://schemas.google.com/g/2010#updates-from", + "type" => "application/atom+xml", + "href" => OStatus.feed_path(user) + }, + %{ + "rel" => "http://ostatus.org/schema/1.0/subscribe", + "template" => OStatus.remote_follow_path() + } + ] + end + + def gather_nodeinfo_protocol_names, do: ["ostatus"] end diff --git a/lib/xml_builder.ex b/lib/xml_builder.ex index 88f8ce2a3..b58602c7b 100644 --- a/lib/xml_builder.ex +++ b/lib/xml_builder.ex @@ -35,6 +35,7 @@ def to_doc(content), do: ~s() <> to_xml(co defp make_open_tag(tag, attributes) do attributes_string = for {attribute, value} <- attributes do + value = String.replace(value, "\"", """) "#{attribute}=\"#{value}\"" end |> Enum.join(" ") diff --git a/mix.exs b/mix.exs index fae21f18d..1396d0072 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", @@ -103,6 +104,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"}, @@ -113,7 +117,10 @@ defp deps do {:recon, github: "ferd/recon", tag: "2.4.0"}, {:quack, "~> 0.1.1"}, {:benchee, "~> 1.0"}, - {:esshd, "~> 0.1.0"} + {:esshd, "~> 0.1.0"}, + {:ex_rated, "~> 1.2"}, + {: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 624c0fb35..cd2d2370a 100644 --- a/mix.lock +++ b/mix.lock @@ -24,17 +24,21 @@ "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, + "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "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"}, @@ -59,6 +63,7 @@ "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, diff --git a/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs b/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs new file mode 100644 index 000000000..d701dcecc --- /dev/null +++ b/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddIndexOnUserInfoDeactivated do + use Ecto.Migration + + def change do + create(index(:users, ["(info->'deactivated')"], name: :users_deactivated_index, using: :gin)) + end +end diff --git a/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs b/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs new file mode 100644 index 000000000..2ffb88cc9 --- /dev/null +++ b/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.ChangeHideColumnInFilterTable do + use Ecto.Migration + + def change do + alter table(:filters) do + modify :hide, :boolean, default: false + end + end +end diff --git a/priv/static/adminfe/favicon.ico b/priv/static/adminfe/favicon.ico new file mode 100644 index 000000000..34b63ac63 Binary files /dev/null and b/priv/static/adminfe/favicon.ico differ diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html new file mode 100644 index 000000000..44a58a44e --- /dev/null +++ b/priv/static/adminfe/index.html @@ -0,0 +1 @@ +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/css/app.cea15678.css b/priv/static/adminfe/static/css/app.cea15678.css new file mode 100644 index 000000000..136aa8bb1 Binary files /dev/null and b/priv/static/adminfe/static/css/app.cea15678.css differ diff --git a/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css new file mode 100644 index 000000000..da819ca09 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css differ diff --git a/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css new file mode 100644 index 000000000..2a2fbd8e4 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css differ diff --git a/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css new file mode 100644 index 000000000..7fa43bf28 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css differ diff --git a/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css new file mode 100644 index 000000000..fbd926db1 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css differ diff --git a/priv/static/adminfe/static/css/chunk-f018.0d22684d.css b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css new file mode 100644 index 000000000..bdb738700 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css differ diff --git a/priv/static/adminfe/static/css/chunk-libs.bd17d456.css b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css new file mode 100644 index 000000000..3a7a99679 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css differ diff --git a/priv/static/adminfe/static/fonts/element-icons.2fad952.woff b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff new file mode 100644 index 000000000..28da65d49 Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff differ diff --git a/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf new file mode 100644 index 000000000..73bc90f4a Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf differ diff --git a/priv/static/adminfe/static/img/401.089007e.gif b/priv/static/adminfe/static/img/401.089007e.gif new file mode 100644 index 000000000..cd6e0d943 Binary files /dev/null and b/priv/static/adminfe/static/img/401.089007e.gif differ diff --git a/priv/static/adminfe/static/img/404.a57b6f3.png b/priv/static/adminfe/static/img/404.a57b6f3.png new file mode 100644 index 000000000..3d8e2305c Binary files /dev/null and b/priv/static/adminfe/static/img/404.a57b6f3.png differ diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js new file mode 100644 index 000000000..4387b8321 Binary files /dev/null and b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js differ diff --git a/priv/static/adminfe/static/js/JEtC.f9ba4594.js b/priv/static/adminfe/static/js/JEtC.f9ba4594.js new file mode 100644 index 000000000..504eaef1f Binary files /dev/null and b/priv/static/adminfe/static/js/JEtC.f9ba4594.js differ diff --git a/priv/static/adminfe/static/js/app.25699e3d.js b/priv/static/adminfe/static/js/app.25699e3d.js new file mode 100644 index 000000000..54694bf5e Binary files /dev/null and b/priv/static/adminfe/static/js/app.25699e3d.js differ diff --git a/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js new file mode 100644 index 000000000..1921d0f64 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js differ diff --git a/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js new file mode 100644 index 000000000..1b5614639 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js differ diff --git a/priv/static/adminfe/static/js/chunk-8b70.46525646.js b/priv/static/adminfe/static/js/chunk-8b70.46525646.js new file mode 100644 index 000000000..68b7ea1a3 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-8b70.46525646.js differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js new file mode 100644 index 000000000..1f40d7e84 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js differ diff --git a/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js new file mode 100644 index 000000000..9c06e442c Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js differ diff --git a/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js new file mode 100644 index 000000000..db0b5dc97 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js differ diff --git a/priv/static/adminfe/static/js/runtime.7144b2cf.js b/priv/static/adminfe/static/js/runtime.7144b2cf.js new file mode 100644 index 000000000..0a58ac351 Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.7144b2cf.js differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js new file mode 100644 index 000000000..e11f322cc Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css new file mode 100644 index 000000000..128237fba Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif new file mode 100644 index 000000000..ba90cc36f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif new file mode 100644 index 000000000..74d897a4f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif new file mode 100644 index 000000000..963a96b8a Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif new file mode 100644 index 000000000..c7cf1011d Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif new file mode 100644 index 000000000..716f55e16 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif new file mode 100644 index 000000000..334d49e0e Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif new file mode 100644 index 000000000..4efd549ed Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif new file mode 100644 index 000000000..82c5b182e Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif new file mode 100644 index 000000000..ca2451e10 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif new file mode 100644 index 000000000..fe66220c2 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif new file mode 100644 index 000000000..fd27edfaa Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif new file mode 100644 index 000000000..0cc9bb71c Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif new file mode 100644 index 000000000..2075dc160 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif new file mode 100644 index 000000000..bef7e2573 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif new file mode 100644 index 000000000..0631c7616 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif new file mode 100644 index 000000000..648e6e879 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css new file mode 100644 index 000000000..96e4d7c5d Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css new file mode 100644 index 000000000..7b45d3397 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css new file mode 100644 index 000000000..bad168cfe Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff new file mode 100644 index 000000000..1e3be038a Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot new file mode 100644 index 000000000..b144ba0bd Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg new file mode 100644 index 000000000..b4ee6f408 --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg @@ -0,0 +1,63 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf new file mode 100644 index 000000000..a983e2dc4 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff new file mode 100644 index 000000000..d8962df76 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot new file mode 100644 index 000000000..5336c38ff Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg new file mode 100644 index 000000000..9fa215f3d --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg @@ -0,0 +1,131 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf new file mode 100644 index 000000000..61a48a511 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff new file mode 100644 index 000000000..aace5d9c5 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif new file mode 100644 index 000000000..606348c7f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif new file mode 100644 index 000000000..c69e93723 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif new file mode 100644 index 000000000..cccd7f023 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif new file mode 100644 index 000000000..388486517 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css new file mode 100644 index 000000000..4ad815bf5 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map new file mode 100644 index 000000000..c8763dcc3 --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["./src/skins/lightgray/main/less/desktop/Reset.less","./src/skins/lightgray/main/less/desktop/Variables.less","./src/skins/lightgray/main/less/desktop/Mixins.less","./src/skins/lightgray/main/less/desktop/Animations.less","./src/skins/lightgray/main/less/desktop/TinyMCE.less","./src/skins/lightgray/main/less/desktop/CropRect.less","./src/skins/lightgray/main/less/desktop/ImagePanel.less","./src/skins/lightgray/main/less/desktop/Arrows.less","./src/skins/lightgray/main/less/desktop/Sidebar.less","./src/skins/lightgray/main/less/desktop/Container.less","./src/skins/lightgray/main/less/desktop/Scrollable.less","./src/skins/lightgray/main/less/desktop/Panel.less","./src/skins/lightgray/main/less/desktop/FloatPanel.less","./src/skins/lightgray/main/less/desktop/Window.less","./src/skins/lightgray/main/less/desktop/ToolTip.less","./src/skins/lightgray/main/less/desktop/Progress.less","./src/skins/lightgray/main/less/desktop/Notification.less","./src/skins/lightgray/main/less/desktop/AbsoluteLayout.less","./src/skins/lightgray/main/less/desktop/Button.less","./src/skins/lightgray/main/less/desktop/ButtonGroup.less","./src/skins/lightgray/main/less/desktop/Checkbox.less","./src/skins/lightgray/main/less/desktop/ComboBox.less","./src/skins/lightgray/main/less/desktop/ColorBox.less","./src/skins/lightgray/main/less/desktop/ColorButton.less","./src/skins/lightgray/main/less/desktop/ColorPicker.less","./src/skins/lightgray/main/less/desktop/Path.less","./src/skins/lightgray/main/less/desktop/FieldSet.less","./src/skins/lightgray/main/less/desktop/FitLayout.less","./src/skins/lightgray/main/less/desktop/FlowLayout.less","./src/skins/lightgray/main/less/desktop/Iframe.less","./src/skins/lightgray/main/less/desktop/InfoBox.less","./src/skins/lightgray/main/less/desktop/Label.less","./src/skins/lightgray/main/less/desktop/MenuBar.less","./src/skins/lightgray/main/less/desktop/MenuButton.less","./src/skins/lightgray/main/less/desktop/MenuItem.less","./src/skins/lightgray/main/less/desktop/Throbber.less","./src/skins/lightgray/main/less/desktop/Menu.less","./src/skins/lightgray/main/less/desktop/ListBox.less","./src/skins/lightgray/main/less/desktop/ResizeHandle.less","./src/skins/lightgray/main/less/desktop/SelectBox.less","./src/skins/lightgray/main/less/desktop/Slider.less","./src/skins/lightgray/main/less/desktop/Spacer.less","./src/skins/lightgray/main/less/desktop/SplitButton.less","./src/skins/lightgray/main/less/desktop/StackLayout.less","./src/skins/lightgray/main/less/desktop/TabPanel.less","./src/skins/lightgray/main/less/desktop/TextBox.less","./src/skins/lightgray/main/less/desktop/DropZone.less","./src/skins/lightgray/main/less/desktop/BrowseButton.less","./src/skins/lightgray/main/less/desktop/Icons.less","./src/skins/lightgray/main/less/desktop/FilePicker.less"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js new file mode 100644 index 000000000..d7fcac80b Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js differ diff --git a/priv/static/images/pleroma-fox-tan-shy.png b/priv/static/images/pleroma-fox-tan-shy.png new file mode 100644 index 000000000..6e24be1e3 Binary files /dev/null and b/priv/static/images/pleroma-fox-tan-shy.png differ diff --git a/test/config_test.exs b/test/config_test.exs index 0a6f0395a..73f3fcb0a 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -28,6 +28,15 @@ test "get/1 with a list of keys" do assert Pleroma.Config.get([:azerty, :uiop], true) == true end + test "get/1 when value is false" do + Pleroma.Config.put([:instance, :false_test], false) + Pleroma.Config.put([:instance, :nested], []) + Pleroma.Config.put([:instance, :nested, :false_test], false) + + assert Pleroma.Config.get([:instance, :false_test]) == false + assert Pleroma.Config.get([:instance, :nested, :false_test]) == false + end + test "get!/1" do assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance) @@ -43,6 +52,15 @@ test "get!/1" do end) end + test "get!/1 when value is false" do + Pleroma.Config.put([:instance, :false_test], false) + Pleroma.Config.put([:instance, :nested], []) + Pleroma.Config.put([:instance, :nested, :false_test], false) + + assert Pleroma.Config.get!([:instance, :false_test]) == false + assert Pleroma.Config.get!([:instance, :nested, :false_test]) == false + end + test "put/2 with a key" do Pleroma.Config.put(:config_test, true) diff --git a/test/conversation_test.exs b/test/conversation_test.exs index cdec18f0f..5903d10ff 100644 --- a/test/conversation_test.exs +++ b/test/conversation_test.exs @@ -4,7 +4,9 @@ defmodule Pleroma.ConversationTest do use Pleroma.DataCase + alias Pleroma.Activity alias Pleroma.Conversation + alias Pleroma.Object alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -154,4 +156,40 @@ test "create_or_bump_for returns the conversation with participations" do assert {:error, _} = Conversation.create_or_bump_for(activity) end + + test "create_or_bump_for does not normalize objects before checking the activity type" do + note = insert(:note) + note_id = note.data["id"] + Repo.delete(note) + refute Object.get_by_ap_id(note_id) + + Tesla.Mock.mock(fn env -> + case env.url do + ^note_id -> + # TODO: add attributedTo and tag to the note factory + body = + note.data + |> Map.put("attributedTo", note.data["actor"]) + |> Map.put("tag", []) + |> Jason.encode!() + + %Tesla.Env{status: 200, body: body} + end + end) + + undo = %Activity{ + id: "fake", + data: %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => note.data["actor"], + "to" => [note.data["actor"]], + "object" => note_id, + "type" => "Undo" + } + } + + Conversation.create_or_bump_for(undo) + + refute Object.get_by_ap_id(note_id) + end end 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_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/rate_limit_plug_test.exs b/test/plugs/rate_limit_plug_test.exs new file mode 100644 index 000000000..2ec9a8fb7 --- /dev/null +++ b/test/plugs/rate_limit_plug_test.exs @@ -0,0 +1,50 @@ +defmodule Pleroma.Plugs.RateLimitPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.RateLimitPlug + + @opts RateLimitPlug.init(%{max_requests: 5, interval: 1}) + + setup do + enabled = Pleroma.Config.get([:app_account_creation, :enabled]) + + Pleroma.Config.put([:app_account_creation, :enabled], true) + + on_exit(fn -> + Pleroma.Config.put([:app_account_creation, :enabled], enabled) + end) + + :ok + end + + test "it restricts by opts" do + conn = conn(:get, "/") + bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") + ms = 1000 + + conn = RateLimitPlug.call(conn, @opts) + {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + assert conn.status == 403 + assert conn.halted + assert conn.resp_body == "{\"error\":\"Rate limit exceeded.\"}" + + Process.sleep(to_reset) + + conn = conn(:get, "/") + conn = RateLimitPlug.call(conn, @opts) + {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + refute conn.status == 403 + refute conn.halted + refute conn.resp_body + end +end diff --git a/test/user_test.exs b/test/user_test.exs index adc77a264..0b65e89e9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.UserTest do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI use Pleroma.DataCase @@ -213,8 +214,8 @@ test "test if a user is following another user" do test "fetches correct profile for nickname beginning with number" do # Use old-style integer ID to try to reproduce the problem user = insert(:user, %{id: 1080}) - userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"}) - assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname) + user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"}) + assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname) end describe "user registration" do @@ -349,7 +350,7 @@ test "it creates unconfirmed user" do end test "it creates confirmed user if :confirmed option is given" do - changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true) + changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false) assert changeset.valid? {:ok, user} = Repo.insert(changeset) @@ -816,13 +817,73 @@ test "get recipients from activity" do assert addressed in recipients end - test ".deactivate can de-activate then re-activate a user" do - user = insert(:user) - assert false == user.info.deactivated - {:ok, user} = User.deactivate(user) - assert true == user.info.deactivated - {:ok, user} = User.deactivate(user, false) - assert false == user.info.deactivated + describe ".deactivate" do + test "can de-activate then re-activate a user" do + user = insert(:user) + assert false == user.info.deactivated + {:ok, user} = User.deactivate(user) + assert true == user.info.deactivated + {:ok, user} = User.deactivate(user, false) + assert false == user.info.deactivated + end + + test "hide a user from followers " do + user = insert(:user) + user2 = insert(:user) + + {:ok, user} = User.follow(user, user2) + {:ok, _user} = User.deactivate(user) + + info = User.get_cached_user_info(user2) + + assert info.follower_count == 0 + assert {:ok, []} = User.get_followers(user2) + end + + test "hide a user from friends" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user2} = User.follow(user2, user) + assert User.following_count(user2) == 1 + + {:ok, _user} = User.deactivate(user) + + info = User.get_cached_user_info(user2) + + assert info.following_count == 0 + assert User.following_count(user2) == 0 + assert {:ok, []} = User.get_friends(user2) + end + + test "hide a user's statuses from timelines and notifications" do + user = insert(:user) + user2 = insert(:user) + + {:ok, user2} = User.follow(user2, user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"}) + + activity = Repo.preload(activity, :bookmark) + + [notification] = Pleroma.Notification.for_user(user2) + assert notification.activity.id == activity.id + + assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark) + + assert [activity] == + ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) + |> ActivityPub.contain_timeline(user2) + + {:ok, _user} = User.deactivate(user) + + assert [] == ActivityPub.fetch_public_activities(%{}) + assert [] == Pleroma.Notification.for_user(user2) + + assert [] == + ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) + |> ActivityPub.contain_timeline(user2) + end end test ".delete_user_activities deletes all create activities" do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 1e056b7ee..0f90aa1ac 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Publisher alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI @@ -963,8 +964,7 @@ test "it filters broken threads" do 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, private_activity_1, private_activity_3] == activities assert length(activities) == 3 @@ -1057,7 +1057,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://200.site/users/nick1/inbox" - assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_reachable(inbox)) end @@ -1070,7 +1070,7 @@ test "it can create a Flag activity" do inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = - ActivityPub.publish_one(%{ + Publisher.publish_one(%{ inbox: inbox, json: "{}", actor: actor, @@ -1089,7 +1089,7 @@ test "it can create a Flag activity" do inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = - ActivityPub.publish_one(%{ + Publisher.publish_one(%{ inbox: inbox, json: "{}", actor: actor, @@ -1107,8 +1107,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://404.site/users/nick1/inbox" - assert {:error, _} = - ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_unreachable(inbox)) end @@ -1120,8 +1119,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://connrefused.site/users/nick1/inbox" - assert {:error, _} = - ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_unreachable(inbox)) end @@ -1133,7 +1131,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://200.site/users/nick1/inbox" - assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) refute called(Instances.set_unreachable(inbox)) end @@ -1146,7 +1144,7 @@ test "it can create a Flag activity" do inbox = "http://connrefused.site/users/nick1/inbox" assert {:error, _} = - ActivityPub.publish_one(%{ + Publisher.publish_one(%{ inbox: inbox, json: "{}", actor: actor, diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index ff0e72401..e2584f635 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -105,4 +105,16 @@ test "doesn't die when the user doesn't exist", 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 b89c42327..6c1897b5a 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.UserInviteToken import Pleroma.Factory - describe "/api/pleroma/admin/user" do + describe "/api/pleroma/admin/users" do test "Delete" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -18,7 +18,7 @@ test "Delete" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/user?nickname=#{user.nickname}") + |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") assert json_response(conn, 200) == user.nickname end @@ -30,7 +30,7 @@ test "Create" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user", %{ + |> post("/api/pleroma/admin/users", %{ "nickname" => "lain", "email" => "lain@example.org", "password" => "test" @@ -75,7 +75,7 @@ test "when the user doesn't exist", %{conn: conn} do end end - describe "/api/pleroma/admin/user/follow" do + describe "/api/pleroma/admin/users/follow" do test "allows to force-follow another user" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -84,7 +84,7 @@ test "allows to force-follow another user" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user/follow", %{ + |> post("/api/pleroma/admin/users/follow", %{ "follower" => follower.nickname, "followed" => user.nickname }) @@ -96,7 +96,7 @@ test "allows to force-follow another user" do end end - describe "/api/pleroma/admin/user/unfollow" do + describe "/api/pleroma/admin/users/unfollow" do test "allows to force-unfollow another user" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -107,7 +107,7 @@ test "allows to force-unfollow another user" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user/unfollow", %{ + |> post("/api/pleroma/admin/users/unfollow", %{ "follower" => follower.nickname, "followed" => user.nickname }) @@ -191,7 +191,7 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user end end - describe "/api/pleroma/admin/permission_group" do + describe "/api/pleroma/admin/users/:nickname/permission_group" do test "GET is giving user_info" do admin = insert(:user, info: %{is_admin: true}) @@ -199,7 +199,7 @@ test "GET is giving user_info" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/permission_group/#{admin.nickname}") + |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") assert json_response(conn, 200) == %{ "is_admin" => true, @@ -215,7 +215,7 @@ test "/:right POST, can add to a permission group" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/permission_group/#{user.nickname}/admin") + |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") assert json_response(conn, 200) == %{ "is_admin" => true @@ -230,7 +230,7 @@ test "/:right DELETE, can remove from a permission group" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/permission_group/#{user.nickname}/admin") + |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") assert json_response(conn, 200) == %{ "is_admin" => false @@ -238,7 +238,7 @@ test "/:right DELETE, can remove from a permission group" do end end - describe "PUT /api/pleroma/admin/activation_status" do + describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do setup %{conn: conn} do admin = insert(:user, info: %{is_admin: true}) @@ -255,7 +255,7 @@ test "deactivates the user", %{conn: conn} do conn = conn - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) user = User.get_cached_by_id(user.id) assert user.info.deactivated == true @@ -267,7 +267,7 @@ test "activates the user", %{conn: conn} do conn = conn - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: true}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true}) user = User.get_cached_by_id(user.id) assert user.info.deactivated == false @@ -280,7 +280,7 @@ test "returns 403 when requested by a non-admin", %{conn: conn} do conn = conn |> assign(:user, user) - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) assert json_response(conn, :forbidden) end @@ -309,7 +309,9 @@ test "sends invitation and returns 204", %{conn: conn, user: user} do conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=#{recipient_email}&name=#{recipient_name}") + |> post( + "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" + ) assert json_response(conn, :no_content) @@ -341,13 +343,13 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do conn = conn |> assign(:user, non_admin_user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :forbidden) end end - describe "POST /api/pleroma/admin/email_invite, with invalid config" do + describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do setup do [user: insert(:user, info: %{is_admin: true})] end @@ -367,7 +369,7 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: u conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end @@ -387,7 +389,7 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: us conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end @@ -405,7 +407,7 @@ test "/api/pleroma/admin/invite_token" do assert conn.status == 200 end - test "/api/pleroma/admin/password_reset" do + test "/api/pleroma/admin/users/:nickname/password_reset" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -413,20 +415,25 @@ test "/api/pleroma/admin/password_reset" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/password_reset?nickname=#{user.nickname}") + |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") assert conn.status == 200 end describe "GET /api/pleroma/admin/users" do - test "renders users array for the first page" do + setup do admin = insert(:user, info: %{is_admin: true}) - user = insert(:user, local: false, tags: ["foo", "bar"]) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/users?page=1") + + {:ok, conn: conn, admin: admin} + end + + test "renders users array for the first page", %{conn: conn, admin: admin} do + user = insert(:user, local: false, tags: ["foo", "bar"]) + conn = get(conn, "/api/pleroma/admin/users?page=1") assert json_response(conn, 200) == %{ "count" => 2, @@ -452,14 +459,10 @@ test "renders users array for the first page" do } end - test "renders empty array for the second page" do - admin = insert(:user, info: %{is_admin: true}) + test "renders empty array for the second page", %{conn: conn} do insert(:user) - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?page=2") + conn = get(conn, "/api/pleroma/admin/users?page=2") assert json_response(conn, 200) == %{ "count" => 2, @@ -468,14 +471,10 @@ test "renders empty array for the second page" do } end - test "regular search" do - admin = insert(:user, info: %{is_admin: true}) + test "regular search", %{conn: conn} do user = insert(:user, nickname: "bob") - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=bo") + conn = get(conn, "/api/pleroma/admin/users?query=bo") assert json_response(conn, 200) == %{ "count" => 1, @@ -493,17 +492,101 @@ test "regular search" do } end - test "regular search with page size" do - admin = insert(:user, info: %{is_admin: true}) + test "search by domain", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by full nickname", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by display name", %{conn: conn} do + user = insert(:user, name: "Display name") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?name=display") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by email", %{conn: conn} do + user = insert(:user, email: "email@example.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "regular search with page size", %{conn: conn} do user = insert(:user, nickname: "aalice") user2 = insert(:user, nickname: "alice") - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=a&page_size=1&page=1") + conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") - assert json_response(conn, 200) == %{ + assert json_response(conn1, 200) == %{ "count" => 2, "page_size" => 1, "users" => [ @@ -518,12 +601,9 @@ test "regular search with page size" do ] } - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=a&page_size=1&page=2") + conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") - assert json_response(conn, 200) == %{ + assert json_response(conn2, 200) == %{ "count" => 2, "page_size" => 1, "users" => [ @@ -566,7 +646,7 @@ test "only local users" do } end - test "only local users with no query" do + test "only local users with no query", %{admin: old_admin} do admin = insert(:user, info: %{is_admin: true}, nickname: "john") user = insert(:user, nickname: "bob") @@ -578,7 +658,7 @@ test "only local users with no query" do |> get("/api/pleroma/admin/users?filters=local") assert json_response(conn, 200) == %{ - "count" => 2, + "count" => 3, "page_size" => 50, "users" => [ %{ @@ -596,6 +676,100 @@ test "only local users with no query" do "roles" => %{"admin" => true, "moderator" => false}, "local" => true, "tags" => [] + }, + %{ + "deactivated" => false, + "id" => old_admin.id, + "local" => true, + "nickname" => old_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "tags" => [] + } + ] + } + end + + test "load only admins", %{conn: conn, admin: admin} do + second_admin = insert(:user, info: %{is_admin: true}) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => admin.local, + "tags" => [] + }, + %{ + "deactivated" => false, + "id" => second_admin.id, + "nickname" => second_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => second_admin.local, + "tags" => [] + } + ] + } + end + + test "load only moderators", %{conn: conn} do + moderator = insert(:user, info: %{is_moderator: true}) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => moderator.id, + "nickname" => moderator.nickname, + "roles" => %{"admin" => false, "moderator" => true}, + "local" => moderator.local, + "tags" => [] + } + ] + } + end + + test "load users with tags list", %{conn: conn} do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => user1.id, + "nickname" => user1.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user1.local, + "tags" => ["first"] + }, + %{ + "deactivated" => false, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user2.local, + "tags" => ["second"] } ] } @@ -650,14 +824,19 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do } end - describe "GET /api/pleroma/admin/invite_token" do - test "without options" do + describe "GET /api/pleroma/admin/users/invite_token" do + setup do admin = insert(:user, info: %{is_admin: true}) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token") + + {:ok, conn: conn} + end + + test "without options", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invite_token") token = json_response(conn, 200) invite = UserInviteToken.find_by_token!(token) @@ -667,13 +846,9 @@ test "without options" do assert invite.invite_type == "one_time" end - test "with expires_at" do - admin = insert(:user, info: %{is_admin: true}) - + test "with expires_at", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"expires_at" => Date.to_string(Date.utc_today())} }) @@ -686,13 +861,9 @@ test "with expires_at" do assert invite.invite_type == "date_limited" end - test "with max_use" do - admin = insert(:user, info: %{is_admin: true}) - + test "with max_use", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"max_use" => 150} }) @@ -704,13 +875,9 @@ test "with max_use" do assert invite.invite_type == "reusable" end - test "with max use and expires_at" do - admin = insert(:user, info: %{is_admin: true}) - + test "with max use and expires_at", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())} }) @@ -723,26 +890,27 @@ test "with max use and expires_at" do end end - describe "GET /api/pleroma/admin/invites" do - test "no invites" do + describe "GET /api/pleroma/admin/users/invites" do + setup do admin = insert(:user, info: %{is_admin: true}) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/invites") + + {:ok, conn: conn} + end + + test "no invites", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invites") assert json_response(conn, 200) == %{"invites" => []} end - test "with invite" do - admin = insert(:user, info: %{is_admin: true}) + test "with invite", %{conn: conn} do {:ok, invite} = UserInviteToken.create_invite() - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invites") + conn = get(conn, "/api/pleroma/admin/users/invites") assert json_response(conn, 200) == %{ "invites" => [ @@ -760,7 +928,7 @@ test "with invite" do end end - describe "POST /api/pleroma/admin/revoke_invite" do + describe "POST /api/pleroma/admin/users/revoke_invite" do test "with token" do admin = insert(:user, info: %{is_admin: true}) {:ok, invite} = UserInviteToken.create_invite() @@ -768,7 +936,7 @@ test "with token" do conn = build_conn() |> assign(:user, admin) - |> post("/api/pleroma/admin/revoke_invite", %{"token" => invite.token}) + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) assert json_response(conn, 200) == %{ "expires_at" => nil, diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs index 3950996ed..501a8d007 100644 --- a/test/web/admin_api/search_test.exs +++ b/test/web/admin_api/search_test.exs @@ -70,11 +70,11 @@ test "it returns active/deactivated users" do test "it returns specific user" do insert(:user) insert(:user) - insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) + user = insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) {:ok, _results, total_count} = Search.user(%{query: ""}) - {:ok, _results, count} = + {:ok, [^user], count} = Search.user(%{ query: "Bo", active: true, @@ -84,5 +84,87 @@ test "it returns specific user" do assert total_count == 3 assert count == 1 end + + test "it returns user by domain" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it return user by full nickname" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it returns admin user" do + admin = insert(:user, info: %{is_admin: true}) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^admin], count} = Search.user(%{is_admin: true}) + assert total == 3 + assert count == 1 + end + + test "it returns moderator user" do + moderator = insert(:user, info: %{is_moderator: true}) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) + assert total == 3 + assert count == 1 + end + + test "it returns users with tags" do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) + assert total == 4 + assert count == 2 + assert user1 in users + assert user2 in users + end + + test "it returns user by display name" do + user = insert(:user, name: "Display name") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{name: "display"}) + + assert total == 3 + assert count == 1 + end + + test "it returns user by email" do + user = insert(:user, email: "some@example.com") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) + + assert total == 3 + assert count == 1 + 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..8d4f401ee 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 diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 52729eb50..0f43bc8f2 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -58,7 +58,7 @@ test "with relays deactivated, it does not publish to the relay", %{ describe "Targets reachability filtering in `publish`" do test_with_mock "it federates only to reachable instances via AP", - Federator, + Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do user = insert(:user) @@ -88,13 +88,18 @@ test "with relays deactivated, it does not publish to the relay", %{ {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) - assert called(Federator.publish_single_ap(%{inbox: inbox1, unreachable_since: dt})) + assert called( + Pleroma.Web.ActivityPub.Publisher.publish_one(%{ + inbox: inbox1, + unreachable_since: dt + }) + ) - refute called(Federator.publish_single_ap(%{inbox: inbox2})) + refute called(Pleroma.Web.ActivityPub.Publisher.publish_one(%{inbox: inbox2})) end test_with_mock "it federates only to reachable instances via Websub", - Federator, + Pleroma.Web.Websub, [:passthrough], [] do user = insert(:user) @@ -122,17 +127,17 @@ test "with relays deactivated, it does not publish to the relay", %{ {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"}) assert called( - Federator.publish_single_websub(%{ + Pleroma.Web.Websub.publish_one(%{ callback: sub2.callback, unreachable_since: dt }) ) - refute called(Federator.publish_single_websub(%{callback: sub1.callback})) + refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback})) end test_with_mock "it federates only to reachable instances via Salmon", - Federator, + Pleroma.Web.Salmon, [:passthrough], [] do user = insert(:user) @@ -162,13 +167,13 @@ test "with relays deactivated, it does not publish to the relay", %{ CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) assert called( - Federator.publish_single_salmon(%{ + Pleroma.Web.Salmon.publish_one(%{ recipient: remote_user2, unreachable_since: dt }) ) - refute called(Federator.publish_single_websub(%{recipient: remote_user1})) + refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1})) 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 505e45010..40e7739e7 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OStatus alias Pleroma.Web.Push alias Pleroma.Web.TwitterAPI.TwitterAPI @@ -80,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) @@ -572,6 +586,7 @@ test "creating a filter", %{conn: conn} do assert response = json_response(conn, 200) assert response["phrase"] == filter.phrase assert response["context"] == filter.context + assert response["irreversible"] == false assert response["id"] != nil assert response["id"] != "" end @@ -3215,4 +3230,129 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id end + + describe "create account by app" do + setup do + enabled = Pleroma.Config.get([:app_account_creation, :enabled]) + max_requests = Pleroma.Config.get([:app_account_creation, :max_requests]) + interval = Pleroma.Config.get([:app_account_creation, :interval]) + + Pleroma.Config.put([:app_account_creation, :enabled], true) + Pleroma.Config.put([:app_account_creation, :max_requests], 5) + Pleroma.Config.put([:app_account_creation, :interval], 1) + + on_exit(fn -> + Pleroma.Config.put([:app_account_creation, :enabled], enabled) + Pleroma.Config.put([:app_account_creation, :max_requests], max_requests) + Pleroma.Config.put([:app_account_creation, :interval], interval) + end) + + :ok + end + + test "Account registration via Application", %{conn: conn} do + conn = + conn + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response(conn, 200) + + conn = + conn + |> post("/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + + assert token_from_db.user.info.confirmation_pending + end + + test "rate limit", %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + put_req_header(conn, "authorization", "Bearer " <> app_token.token) + |> Map.put(:remote_ip, {15, 15, 15, 15}) + + for i <- 1..5 do + conn = + conn + |> post("/api/v1/accounts", %{ + username: "#{i}lain", + email: "#{i}lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + + assert token_from_db.user.info.confirmation_pending + end + + conn = + conn + |> post("/api/v1/accounts", %{ + username: "6lain", + email: "6lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."} + end + end end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index cb6836983..1c04ac9ad 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -614,6 +614,27 @@ test "issues a token for request with HTTP basic auth client credentials" do assert token.scopes == ["scope1", "scope2"] end + test "issue a token for client_credentials grant type" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write" + end + test "rejects token exchange with invalid client credentials" do user = insert(:user) app = insert(:oauth_app) @@ -644,7 +665,7 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user password = "testpassword" user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) - info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed) + info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true) {:ok, user} = user diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs new file mode 100644 index 000000000..20e338cab --- /dev/null +++ b/test/web/oauth/token/utils_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Token.UtilsTest do + use Pleroma.DataCase + alias Pleroma.Web.OAuth.Token.Utils + import Pleroma.Factory + + describe "fetch_app/1" do + test "returns error when credentials is invalid" do + assert {:error, :not_found} = + Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}}) + end + + test "returns App by params credentails" do + app = insert(:oauth_app) + + assert {:ok, load_app} = + Utils.fetch_app(%Plug.Conn{ + params: %{"client_id" => app.client_id, "client_secret" => app.client_secret} + }) + + assert load_app == app + end + + test "returns App by header credentails" do + app = insert(:oauth_app) + header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}") + + conn = + %Plug.Conn{} + |> Plug.Conn.put_req_header("authorization", header) + + assert {:ok, load_app} = Utils.fetch_app(conn) + assert load_app == app + end + end + + describe "format_created_at/1" do + test "returns formatted created at" do + token = insert(:oauth_token) + date = Utils.format_created_at(token) + + token_date = + token.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + + assert token_date == date + end + end +end diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs index a4bb68c4d..16ee02abb 100644 --- a/test/web/ostatus/activity_representer_test.exs +++ b/test/web/ostatus/activity_representer_test.exs @@ -67,37 +67,31 @@ test "a note activity" do end test "a reply note" do - note = insert(:note_activity) - answer = insert(:note_activity) - object = answer.data["object"] - object = Map.put(object, "inReplyTo", note.data["object"]["id"]) - - data = %{answer.data | "object" => object} - answer = %{answer | data: data} - - note_object = Object.get_by_ap_id(note.data["object"]["id"]) + user = insert(:user) + note_object = insert(:note) + _note = insert(:note_activity, %{note: note_object}) + object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}}) + answer = insert(:note_activity, %{note: object}) Repo.update!( Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")}) ) - user = User.get_cached_by_ap_id(answer.data["actor"]) - expected = """ http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post - #{answer.data["object"]["id"]} + #{object.data["id"]} New note by #{user.nickname} - #{answer.data["object"]["content"]} - #{answer.data["object"]["published"]} - #{answer.data["object"]["published"]} + #{object.data["content"]} + #{object.data["published"]} + #{object.data["published"]} #{answer.data["context"]} 2hu - - + + - + """ diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs index 7532578ca..232082779 100644 --- a/test/web/salmon/salmon_test.exs +++ b/test/web/salmon/salmon_test.exs @@ -7,7 +7,9 @@ defmodule Pleroma.Web.Salmon.SalmonTest do alias Pleroma.Activity alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Salmon + import Mock import Pleroma.Factory @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" @@ -77,7 +79,10 @@ test "it gets a magic key" do "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB" end - test "it pushes an activity to remote accounts it's addressed to" do + test_with_mock "it pushes an activity to remote accounts it's addressed to", + Publisher, + [:passthrough], + [] do user_data = %{ info: %{ salmon: "http://test-example.org/salmon" @@ -102,10 +107,8 @@ test "it pushes an activity to remote accounts it's addressed to" do user = User.get_cached_by_ap_id(activity.data["actor"]) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) - poster = fn url, _data, _headers -> - assert url == "http://test-example.org/salmon" - end + Salmon.publish(user, activity) - Salmon.publish(user, activity, poster) + assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user})) end end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 90718cfb4..e194f14fb 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -1094,7 +1094,7 @@ test "it returns 500 when user is not local", %{conn: conn, user: user} do describe "GET /api/account/confirm_email/:id/:token" do setup do user = insert(:user) - info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) + info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) {:ok, user} = user @@ -1145,7 +1145,7 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do end user = insert(:user) - info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) + info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) {:ok, user} = user diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 56474447b..2cd82b3e7 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -141,7 +141,7 @@ test "returns the state of safe_dm_mentions flag", %{conn: conn} do test "it returns the managed config", %{conn: conn} do Pleroma.Config.put([:instance, :managed_config], false) - Pleroma.Config.put([:fe], theme: "rei-ayanami-towel") + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) response = conn @@ -157,29 +157,7 @@ test "it returns the managed config", %{conn: conn} do |> get("/api/statusnet/config.json") |> json_response(:ok) - assert response["site"]["pleromafe"] - end - - test "if :pleroma, :fe is false, it returns the new style config settings", %{conn: conn} do - Pleroma.Config.put([:instance, :managed_config], true) - Pleroma.Config.put([:fe, :theme], "rei-ayanami-towel") - Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["pleromafe"]["theme"] == "rei-ayanami-towel" - - Pleroma.Config.put([:fe], false) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["pleromafe"]["theme"] == "asuka-hospital" + assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"} end end @@ -251,4 +229,22 @@ test "GET /api/pleroma/healthcheck", %{conn: conn} do assert conn.status in [200, 503] end + + describe "POST /api/pleroma/disable_account" do + test "it returns HTTP 200", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/api/pleroma/disable_account", %{"password" => "test"}) + |> json_response(:ok) + + assert response == %{"status" => "success"} + + user = User.get_cached_by_id(user.id) + + assert user.info.deactivated == true + end + end end diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs index 1aa533b48..43bd77f78 100644 --- a/test/web/twitter_api/views/activity_view_test.exs +++ b/test/web/twitter_api/views/activity_view_test.exs @@ -295,8 +295,8 @@ test "an announce activity" do "id" => announce.id, "is_local" => true, "is_post_verb" => false, - "statusnet_html" => "shp retweeted a status.", - "text" => "shp retweeted a status.", + "statusnet_html" => "shp repeated a status.", + "text" => "shp repeated a status.", "uri" => "tag:#{announce.data["id"]}:objectType=note", "user" => UserView.render("show.json", user: other_user), "retweeted_status" => ActivityView.render("activity.json", activity: activity), diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs index d529fd2c3..3857d585f 100644 --- a/test/web/views/error_view_test.exs +++ b/test/web/views/error_view_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ErrorViewTest do use Pleroma.Web.ConnCase, async: true + import ExUnit.CaptureLog # Bring render/3 and render_to_string/3 for testing custom views import Phoenix.View @@ -13,17 +14,23 @@ test "renders 404.json" do end test "render 500.json" do - assert render(Pleroma.Web.ErrorView, "500.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" end test "render any other" do - assert render(Pleroma.Web.ErrorView, "505.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "505.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" end test "render 500.json with reason" do - assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == - %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == + %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} + end) =~ "[error] Internal server error: \"test reason\"" end end