diff options
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-rw-r--r-- | docs/api/admin_api.md | 23 | ||||
-rw-r--r-- | lib/mix/tasks/pleroma/config.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/flake_id.ex | 10 | ||||
-rw-r--r-- | lib/pleroma/user.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/admin_api_controller.ex | 10 | ||||
-rw-r--r-- | lib/pleroma/web/common_api/utils.ex | 7 | ||||
-rw-r--r-- | lib/pleroma/web/ostatus/handlers/follow_handler.ex | 6 | ||||
-rw-r--r-- | lib/pleroma/web/ostatus/handlers/note_handler.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/ostatus/handlers/unfollow_handler.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/ostatus/ostatus.ex | 17 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/twitter_api/controllers/util_controller.ex | 197 | ||||
-rw-r--r-- | mix.exs | 23 | ||||
-rw-r--r-- | test/flake_id_test.exs | 5 | ||||
-rw-r--r-- | test/support/http_request_mock.ex | 4 | ||||
-rw-r--r-- | test/web/admin_api/admin_api_controller_test.exs | 40 | ||||
-rw-r--r-- | test/web/common_api/common_api_utils_test.exs | 20 | ||||
-rw-r--r-- | test/web/ostatus/ostatus_test.exs | 56 | ||||
-rw-r--r-- | test/web/twitter_api/util_controller_test.exs | 404 |
20 files changed, 720 insertions, 118 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e77fe4f3d..6fdc432ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Security +- OStatus: eliminate the possibility of a protocol downgrade attack. +- OStatus: prevent following locked accounts, bypassing the approval process. + ### Changed - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config - **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired @@ -30,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification. - ActivityPub S2S: remote user deletions now work the same as local user deletions. - Not being able to access the Mastodon FE login page on private instances +- Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) @@ -53,6 +58,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: Return avatar and display name when querying users - Admin API: Allow querying user by ID - Admin API: Added support for `tuples`. +- Admin API: Added endpoints to run mix tasks pleroma.config migrate_to_db & pleroma.config migrate_from_db - Added synchronization of following/followers counters for external users - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. - Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index ca9303227..22873dde9 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -575,6 +575,29 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - 404 Not Found `"Not found"` - On success: 200 OK `{}` + +## `/api/pleroma/admin/config/migrate_to_db` +### Run mix task pleroma.config migrate_to_db +Copy settings on key `:pleroma` to DB. +- Method `GET` +- Params: none +- Response: + +```json +{} +``` + +## `/api/pleroma/admin/config/migrate_from_db` +### Run mix task pleroma.config migrate_from_db +Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB. +- Method `GET` +- Params: none +- Response: + +```json +{} +``` + ## `/api/pleroma/admin/config` ### List config settings List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index a7d0fac5d..462940e7e 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -15,7 +15,7 @@ defmodule Mix.Tasks.Pleroma.Config do mix pleroma.config migrate_to_db - ## Transfers config from DB to file. + ## Transfers config from DB to file `config/env.exported_from_db.secret.exs` mix pleroma.config migrate_from_db ENV """ diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex index 58ab3650d..ca0610abc 100644 --- a/lib/pleroma/flake_id.ex +++ b/lib/pleroma/flake_id.ex @@ -66,6 +66,16 @@ defmodule Pleroma.FlakeId do @spec get :: binary def get, do: to_string(:gen_server.call(:flake, :get)) + # checks that ID is is valid FlakeID + # + @spec is_flake_id?(String.t()) :: boolean + def is_flake_id?(id), do: is_flake_id?(String.to_charlist(id), true) + defp is_flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: is_flake_id?(cs, true) + defp is_flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: is_flake_id?(cs, true) + defp is_flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: is_flake_id?(cs, true) + defp is_flake_id?([], true), do: true + defp is_flake_id?(_, _), do: false + # -- Ecto.Type API @impl Ecto.Type def type, do: :uuid diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 69835f3dd..974f96852 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -228,6 +228,7 @@ defmodule Pleroma.User do |> put_password_hash end + @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def reset_password(%User{id: user_id} = user, data) do multi = Multi.new() @@ -332,6 +333,7 @@ defmodule Pleroma.User do def needs_update?(_), do: true + @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do {:ok, follower} end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 1ae5acd91..fcda57b3e 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -379,6 +379,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + def migrate_to_db(conn, _params) do + Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) + json(conn, %{}) + end + + def migrate_from_db(conn, _params) do + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"]) + json(conn, %{}) + end + def config_show(conn, _params) do configs = Pleroma.Repo.all(Config) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d80fffa26..c8a743e8e 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -24,7 +24,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do # This is a hack for twidere. def get_by_id_or_ap_id(id) do activity = - Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id) + with true <- Pleroma.FlakeId.is_flake_id?(id), + %Activity{} = activity <- Activity.get_by_id_with_object(id) do + activity + else + _ -> Activity.get_create_by_object_ap_id_with_object(id) + end activity && if activity.data["type"] == "Create" do diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex index 263d3b2dc..24513972e 100644 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex @@ -9,14 +9,18 @@ defmodule Pleroma.Web.OStatus.FollowHandler do alias Pleroma.Web.XML def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_user(doc), + with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), {:ok, followed} <- OStatus.find_or_make_user(followed_uri), + {:locked, false} <- {:locked, followed.info.locked}, {:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do User.follow(actor, followed) {:ok, activity} + else + {:locked, true} -> + {:error, "It's not possible to follow locked accounts over OStatus"} end end end diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index 3005e8f57..7fae14f7b 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -111,7 +111,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do with id <- XML.string_from_xpath("//id", entry), activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id), [author] <- :xmerl_xpath.string('//author[1]', doc), - {:ok, actor} <- OStatus.find_make_or_update_user(author), + {:ok, actor} <- OStatus.find_make_or_update_actor(author), content_html <- OStatus.get_content(entry), cw <- OStatus.get_cw(entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex index 6596ada3b..2062432e3 100644 --- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.OStatus.UnfollowHandler do alias Pleroma.Web.XML def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_user(doc), + with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 502410c83..331cbc0b7 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -56,7 +56,7 @@ defmodule Pleroma.Web.OStatus do def handle_incoming(xml_string, options \\ []) do with doc when doc != :error <- parse_document(xml_string) do - with {:ok, actor_user} <- find_make_or_update_user(doc), + with {:ok, actor_user} <- find_make_or_update_actor(doc), do: Pleroma.Instances.set_reachable(actor_user.ap_id) entries = :xmerl_xpath.string('//entry', doc) @@ -120,7 +120,7 @@ defmodule Pleroma.Web.OStatus do end def make_share(entry, doc, retweeted_activity) do - with {:ok, actor} <- find_make_or_update_user(doc), + with {:ok, actor} <- find_make_or_update_actor(doc), %Object{} = object <- Object.normalize(retweeted_activity), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do @@ -138,7 +138,7 @@ defmodule Pleroma.Web.OStatus do end def make_favorite(entry, doc, favorited_activity) do - with {:ok, actor} <- find_make_or_update_user(doc), + with {:ok, actor} <- find_make_or_update_actor(doc), %Object{} = object <- Object.normalize(favorited_activity), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do @@ -264,11 +264,18 @@ defmodule Pleroma.Web.OStatus do end end - def find_make_or_update_user(doc) do + def find_make_or_update_actor(doc) do uri = string_from_xpath("//author/uri[1]", doc) - with {:ok, user} <- find_or_make_user(uri) do + with {:ok, %User{} = user} <- find_or_make_user(uri), + {:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do maybe_update(doc, user) + else + {:ap_enabled, true} -> + {:error, :invalid_protocol} + + _ -> + {:error, :unknown_user} end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 0689d69fb..d475fc973 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -196,6 +196,8 @@ defmodule Pleroma.Web.Router do get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) + get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) + get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) end scope "/", Pleroma.Web.TwitterAPI do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 39bc6147c..3405bd3b7 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -15,10 +15,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User alias Pleroma.Web - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger + plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) + def help_test(conn, _params) do json(conn, "ok") end @@ -59,27 +60,25 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) redirect(conn, to: "/notice/#{activity_id}") else - {err, followee} = User.get_or_fetch(acct) - avatar = User.avatar_url(followee) - name = followee.nickname - id = followee.id - - if !!user do + with {:ok, followee} <- User.get_or_fetch(acct) do conn - |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) - else - conn - |> render("follow_login.html", %{ + |> render(follow_template(user), %{ error: false, acct: acct, - avatar: avatar, - name: name, - id: id + avatar: User.avatar_url(followee), + name: followee.nickname, + id: followee.id }) + else + {:error, _reason} -> + render(conn, follow_template(user), %{error: :error}) end end end + defp follow_template(%User{} = _user), do: "follow.html" + defp follow_template(_), do: "follow_login.html" + defp is_status?(acct) do case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] -> @@ -93,50 +92,53 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do def do_remote_follow(conn, %{ "authorization" => %{"name" => username, "password" => password, "id" => id} }) do - followee = User.get_cached_by_id(id) - avatar = User.avatar_url(followee) - name = followee.nickname - - with %User{} = user <- User.get_cached_by_nickname(username), - true <- AuthenticationPlug.checkpw(password, user.password_hash), - %User{} = _followed <- User.get_cached_by_id(id), - {:ok, follower} <- User.follow(user, followee), - {:ok, _activity} <- ActivityPub.follow(follower, followee) do + with %User{} = followee <- User.get_cached_by_id(id), + {_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee}, + {_, true, _} <- { + :auth, + AuthenticationPlug.checkpw(password, user.password_hash), + followee + }, + {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else # Was already following user {:error, "Could not follow user:" <> _rest} -> - render(conn, "followed.html", %{error: false}) + render(conn, "followed.html", %{error: "Error following account"}) - _e -> + {:auth, _, followee} -> conn |> render("follow_login.html", %{ error: "Wrong username or password", id: id, - name: name, - avatar: avatar + name: followee.nickname, + avatar: User.avatar_url(followee) }) + + e -> + Logger.debug("Remote follow failed with error #{inspect(e)}") + render(conn, "followed.html", %{error: "Something went wrong."}) end end def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do - with %User{} = followee <- User.get_cached_by_id(id), - {:ok, follower} <- User.follow(user, followee), - {:ok, _activity} <- ActivityPub.follow(follower, followee) do + with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, + {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else # Was already following user {:error, "Could not follow user:" <> _rest} -> - conn - |> render("followed.html", %{error: false}) + render(conn, "followed.html", %{error: "Error following account"}) + + {:fetch_user, error} -> + Logger.debug("Remote follow failed with error #{inspect(error)}") + render(conn, "followed.html", %{error: "Could not find user"}) e -> Logger.debug("Remote follow failed with error #{inspect(e)}") - - conn - |> render("followed.html", %{error: inspect(e)}) + render(conn, "followed.html", %{error: "Something went wrong."}) end end @@ -151,67 +153,70 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end end - def config(conn, _params) do + def config(%{assigns: %{format: "xml"}} = conn, _params) do instance = Pleroma.Config.get(:instance) - case get_format(conn) do - "xml" -> - response = """ - <config> - <site> - <name>#{Keyword.get(instance, :name)}</name> - <site>#{Web.base_url()}</site> - <textlimit>#{Keyword.get(instance, :limit)}</textlimit> - <closed>#{!Keyword.get(instance, :registrations_open)}</closed> - </site> - </config> - """ + response = """ + <config> + <site> + <name>#{Keyword.get(instance, :name)}</name> + <site>#{Web.base_url()}</site> + <textlimit>#{Keyword.get(instance, :limit)}</textlimit> + <closed>#{!Keyword.get(instance, :registrations_open)}</closed> + </site> + </config> + """ - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) + conn + |> put_resp_content_type("application/xml") + |> send_resp(200, response) + end - _ -> - vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - - uploadlimit = %{ - uploadlimit: to_string(Keyword.get(instance, :upload_limit)), - avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), - backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), - bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) - } - - data = %{ - name: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - server: Web.base_url(), - textlimit: to_string(Keyword.get(instance, :limit)), - uploadlimit: uploadlimit, - closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"), - private: if(Keyword.get(instance, :public, true), do: "0", else: "1"), - vapidPublicKey: vapid_public_key, - accountActivationRequired: - if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"), - invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"), - safeDMMentionsEnabled: - if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0") - } + def config(conn, _params) do + instance = Pleroma.Config.get(:instance) + vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + + uploadlimit = %{ + uploadlimit: to_string(Keyword.get(instance, :upload_limit)), + avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), + backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), + bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) + } + + data = %{ + name: Keyword.get(instance, :name), + description: Keyword.get(instance, :description), + server: Web.base_url(), + textlimit: to_string(Keyword.get(instance, :limit)), + uploadlimit: uploadlimit, + closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), + private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), + vapidPublicKey: vapid_public_key, + accountActivationRequired: + bool_to_val(Keyword.get(instance, :account_activation_required, false)), + invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), + safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) + } + + managed_config = Keyword.get(instance, :managed_config) + + data = + if managed_config do pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) + Map.put(data, "pleromafe", pleroma_fe) + else + data + end - managed_config = Keyword.get(instance, :managed_config) - - data = - if managed_config do - data |> Map.put("pleromafe", pleroma_fe) - else - data - end - - json(conn, %{site: data}) - end + json(conn, %{site: data}) end + defp bool_to_val(true), do: "1" + defp bool_to_val(_), do: "0" + defp bool_to_val(true, val, _), do: val + defp bool_to_val(_, _, val), do: val + def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) @@ -220,20 +225,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do json(conn, config) end - def version(conn, _params) do + def version(%{assigns: %{format: "xml"}} = conn, _params) do version = Pleroma.Application.named_version() - case get_format(conn) do - "xml" -> - response = "<version>#{version}</version>" - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) + conn + |> put_resp_content_type("application/xml") + |> send_resp(200, "<version>#{version}</version>") + end - _ -> - json(conn, version) - end + def version(conn, _params) do + json(conn, Pleroma.Application.named_version()) end def emoji(conn, _params) do @@ -190,12 +190,13 @@ defmodule Pleroma.Mixfile do tag = String.trim(tag), {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]), describe = String.trim(describe), - ahead <- String.replace(describe, tag, "") do + ahead <- String.replace(describe, tag, ""), + ahead <- String.trim_leading(ahead, "-") do {String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))} else _ -> {commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) - {nil, "-0-g" <> String.trim(commit_hash)} + {nil, "0-g" <> String.trim(commit_hash)} end if git_tag && version != git_tag do @@ -207,14 +208,15 @@ defmodule Pleroma.Mixfile do # Branch name as pre-release version component, denoted with a dot branch_name = with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), + branch_name <- String.trim(branch_name), branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name, - true <- branch_name != "master" do + true <- branch_name not in ["master", "HEAD"] do branch_name = branch_name |> String.trim() |> String.replace(identifier_filter, "-") - "." <> branch_name + branch_name end build_name = @@ -234,6 +236,17 @@ defmodule Pleroma.Mixfile do env_override -> env_override end + # Pre-release version, denoted by appending a hyphen + # and a series of dot separated identifiers + pre_release = + [git_pre_release, branch_name] + |> Enum.filter(fn string -> string && string != "" end) + |> Enum.join(".") + |> (fn + "" -> nil + string -> "-" <> String.replace(string, identifier_filter, "-") + end).() + # Build metadata, denoted with a plus sign build_metadata = [build_name, env_name] @@ -244,7 +257,7 @@ defmodule Pleroma.Mixfile do string -> "+" <> String.replace(string, identifier_filter, "-") end).() - [version, git_pre_release, branch_name, build_metadata] + [version, pre_release, build_metadata] |> Enum.filter(fn string -> string && string != "" end) |> Enum.join() end diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs index ca2338041..85ed5bbdf 100644 --- a/test/flake_id_test.exs +++ b/test/flake_id_test.exs @@ -39,4 +39,9 @@ defmodule Pleroma.FlakeIdTest do assert dump(flake_s) == {:ok, flake} assert dump(flake) == {:ok, flake} end + + test "is_flake_id?/1" do + assert is_flake_id?("9eoozpwTul5mjSEDRI") + refute is_flake_id?("http://example.com/activities/3ebbadd1-eb14-4e20-8118-b6f79c0c7b0b") + end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index bdfe43b28..3adb5ba3b 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -51,6 +51,10 @@ defmodule HttpRequestMock do }} end + def get("https://mastodon.social/users/not_found", _, _, _) do + {:ok, %Tesla.Env{status: 404}} + end + def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 6dda4ae51..f61499a22 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1916,6 +1916,46 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end + describe "config mix tasks run" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + temp_file = "config/test.exported_from_db.secret.exs" + + Mix.shell(Mix.Shell.Quiet) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + :ok = File.rm(temp_file) + end) + + dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) + + Pleroma.Config.put([:instance, :dynamic_configuration], true) + + on_exit(fn -> + Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) + end) + + %{conn: assign(conn, :user, admin), admin: admin} + end + + test "transfer settings to DB and to file", %{conn: conn, admin: admin} do + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] + conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") + assert json_response(conn, 200) == %{} + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0 + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/config/migrate_from_db") + + assert json_response(conn, 200) == %{} + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] + end + end + describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do admin = insert(:user, info: %{is_admin: true}) diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index af320f31f..4b5666c29 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -360,4 +360,24 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do assert third_user.ap_id in to end end + + describe "get_by_id_or_ap_id/1" do + test "get activity by id" do + activity = insert(:note_activity) + %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id) + assert note.id == activity.id + end + + test "get activity by ap_id" do + activity = insert(:note_activity) + %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"]) + assert note.id == activity.id + end + + test "get activity by object when type isn't `Create` " do + activity = insert(:like_activity) + %Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id) + assert like.data["object"] == activity.data["object"] + end + end end diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index 4e8f3a0fc..f8d389020 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -326,6 +326,14 @@ defmodule Pleroma.Web.OStatusTest do assert User.following?(follower, followed) end + test "refuse following over OStatus if the followed's account is locked" do + incoming = File.read!("test/fixtures/follow.xml") + _user = insert(:user, info: %{locked: true}, ap_id: "https://pawoo.net/users/pekorino") + + {:ok, [{:error, "It's not possible to follow locked accounts over OStatus"}]} = + OStatus.handle_incoming(incoming) + end + test "handle incoming unfollows with existing follow" do incoming_follow = File.read!("test/fixtures/follow.xml") {:ok, [_activity]} = OStatus.handle_incoming(incoming_follow) @@ -426,7 +434,7 @@ defmodule Pleroma.Web.OStatusTest do } end - test "find_make_or_update_user takes an author element and returns an updated user" do + test "find_make_or_update_actor takes an author element and returns an updated user" do uri = "https://social.heldscal.la/user/23211" {:ok, user} = OStatus.find_or_make_user(uri) @@ -439,14 +447,56 @@ defmodule Pleroma.Web.OStatusTest do doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) [author] = :xmerl_xpath.string('//author[1]', doc) - {:ok, user} = OStatus.find_make_or_update_user(author) + {:ok, user} = OStatus.find_make_or_update_actor(author) assert user.avatar["type"] == "Image" assert user.name == old_name assert user.bio == old_bio - {:ok, user_again} = OStatus.find_make_or_update_user(author) + {:ok, user_again} = OStatus.find_make_or_update_actor(author) assert user_again == user end + + test "find_or_make_user disallows protocol downgrade" do + user = insert(:user, %{local: true}) + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + + assert User.ap_enabled?(user) + + user = + insert(:user, %{ + ap_id: "https://social.heldscal.la/user/23211", + info: %{ap_enabled: true}, + local: false + }) + + assert User.ap_enabled?(user) + + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + assert User.ap_enabled?(user) + end + + test "find_make_or_update_actor disallows protocol downgrade" do + user = insert(:user, %{local: true}) + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + + assert User.ap_enabled?(user) + + user = + insert(:user, %{ + ap_id: "https://social.heldscal.la/user/23211", + info: %{ap_enabled: true}, + local: false + }) + + assert User.ap_enabled?(user) + + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + assert User.ap_enabled?(user) + + doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) + [author] = :xmerl_xpath.string('//author[1]', doc) + {:error, :invalid_protocol} = OStatus.find_make_or_update_actor(author) + end end describe "gathering user info from a user id" do diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 3d699e1df..640579c09 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -14,6 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do setup do Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + instance_config = Pleroma.Config.get([:instance]) + pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + + on_exit(fn -> + Pleroma.Config.put([:instance], instance_config) + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], pleroma_fe) + Pleroma.Config.put([:user, :deny_follow_blocked], deny_follow_blocked) + end) + :ok end @@ -31,6 +42,35 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do assert response == "job started" end + test "it imports follow lists from file", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + + with_mocks([ + {File, [], + read!: fn "follow_list.txt" -> + "Account address,Show boosts\n#{user2.ap_id},true" + end}, + {PleromaJobQueue, [:passthrough], []} + ]) do + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) + |> json_response(:ok) + + assert called( + PleromaJobQueue.enqueue( + :background, + User, + [:follow_import, user1, [user2.ap_id]] + ) + ) + + assert response == "job started" + end + end + test "it imports new-style mastodon follow lists", %{conn: conn} do user1 = insert(:user) user2 = insert(:user) @@ -79,6 +119,33 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do assert response == "job started" end + + test "it imports blocks users from file", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + with_mocks([ + {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}, + {PleromaJobQueue, [:passthrough], []} + ]) do + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) + |> json_response(:ok) + + assert called( + PleromaJobQueue.enqueue( + :background, + User, + [:blocks_import, user1, [user2.ap_id, user3.ap_id]] + ) + ) + + assert response == "job started" + end + end end describe "POST /api/pleroma/notifications/read" do @@ -98,6 +165,18 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do assert Repo.get(Notification, notification1.id).seen refute Repo.get(Notification, notification2.id).seen end + + test "it returns error when notification not found", %{conn: conn} do + user1 = insert(:user) + + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/notifications/read", %{"id" => "22222222222222"}) + |> json_response(403) + + assert response == %{"error" => "Cannot get notification"} + end end describe "PUT /api/pleroma/notification_settings" do @@ -123,7 +202,63 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end end - describe "GET /api/statusnet/config.json" do + describe "GET /api/statusnet/config" do + test "it returns config in xml format", %{conn: conn} do + instance = Pleroma.Config.get(:instance) + + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/api/statusnet/config") + |> response(:ok) + + assert response == + "<config>\n<site>\n<name>#{Keyword.get(instance, :name)}</name>\n<site>#{ + Pleroma.Web.base_url() + }</site>\n<textlimit>#{Keyword.get(instance, :limit)}</textlimit>\n<closed>#{ + !Keyword.get(instance, :registrations_open) + }</closed>\n</site>\n</config>\n" + end + + test "it returns config in json format", %{conn: conn} do + instance = Pleroma.Config.get(:instance) + Pleroma.Config.put([:instance, :managed_config], true) + Pleroma.Config.put([:instance, :registrations_open], false) + Pleroma.Config.put([:instance, :invites_enabled], true) + Pleroma.Config.put([:instance, :public], false) + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) + + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/statusnet/config") + |> json_response(:ok) + + expected_data = %{ + "site" => %{ + "accountActivationRequired" => "0", + "closed" => "1", + "description" => Keyword.get(instance, :description), + "invitesEnabled" => "1", + "name" => Keyword.get(instance, :name), + "pleromafe" => %{"theme" => "asuka-hospital"}, + "private" => "1", + "safeDMMentionsEnabled" => "0", + "server" => Pleroma.Web.base_url(), + "textlimit" => to_string(Keyword.get(instance, :limit)), + "uploadlimit" => %{ + "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)), + "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)), + "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)), + "uploadlimit" => to_string(Keyword.get(instance, :upload_limit)) + }, + "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + } + + assert response == expected_data + end + test "returns the state of safe_dm_mentions flag", %{conn: conn} do option = Pleroma.Config.get([:instance, :safe_dm_mentions]) Pleroma.Config.put([:instance, :safe_dm_mentions], true) @@ -210,7 +345,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end end - describe "GET /ostatus_subscribe?acct=...." do + describe "GET /ostatus_subscribe - remote_follow/2" do test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do conn = get( @@ -230,6 +365,172 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do assert html_response(response, 200) =~ "Log in to follow" end + + test "show follow page if the `acct` is a account link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get("/ostatus_subscribe?acct=https://mastodon.social/users/emelie") + + assert html_response(response, 200) =~ "Remote follow" + end + + test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get("/ostatus_subscribe?acct=https://mastodon.social/users/not_found") + + assert html_response(response, 200) =~ "Error fetching user" + end + end + + describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user " do + test "follows user", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Account followed!" + assert user2.follower_address in refresh_record(user).following + end + + test "returns error when user is deactivated", %{conn: conn} do + user = insert(:user, info: %{deactivated: true}) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + + {:ok, _user} = Pleroma.User.block(user2, user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => "jimm"}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns success result when user already in followers", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + {:ok, _, _, _} = CommonAPI.follow(user, user2) + + response = + conn + |> assign(:user, refresh_record(user)) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Account followed!" + end + end + + describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user " do + test "follows", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Account followed!" + assert user2.follower_address in refresh_record(user).following + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} + }) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when login invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when password invalid", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + {:ok, _user} = Pleroma.User.block(user2, user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Error following account" + end end describe "GET /api/pleroma/healthcheck" do @@ -311,5 +612,104 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do assert user.info.deactivated == true end + + test "it returns returns when password invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/api/pleroma/disable_account", %{"password" => "test1"}) + |> json_response(:ok) + + assert response == %{"error" => "Invalid password."} + user = User.get_cached_by_id(user.id) + + refute user.info.deactivated + end + end + + describe "GET /api/statusnet/version" do + test "it returns version in xml format", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/api/statusnet/version") + |> response(:ok) + + assert response == "<version>#{Pleroma.Application.named_version()}</version>" + end + + test "it returns version in json format", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/statusnet/version") + |> json_response(:ok) + + assert response == "#{Pleroma.Application.named_version()}" + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when use not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + test "it returns new captcha", %{conn: conn} do + with_mock Pleroma.Captcha, + new: fn -> "test_captcha" end do + resp = + conn + |> get("/api/pleroma/captcha") + |> response(200) + + assert resp == "\"test_captcha\"" + assert called(Pleroma.Captcha.new()) + end end end |