From a9b1589528023c9d71575c936cd1bd52daebbe5e Mon Sep 17 00:00:00 2001 From: tusooa Date: Tue, 3 Jan 2023 21:31:07 -0500 Subject: [PATCH] Validate multilang map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../object_validators/map_of_string.ex | 4 +- lib/pleroma/web/activity_pub/activity_pub.ex | 18 ++- lib/pleroma/web/common_api/activity_draft.ex | 20 +++- lib/pleroma/web/common_api/utils.ex | 15 +++ .../controllers/media_controller.ex | 10 ++ .../controllers/media_controller_test.exs | 42 ++++++- .../controllers/status_controller_test.exs | 110 ++++++++++++++++++ 7 files changed, 209 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/map_of_string.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/map_of_string.ex index f365772ba2..8b6f1741ee 100644 --- a/lib/pleroma/ecto_type/activity_pub/object_validators/map_of_string.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/map_of_string.ex @@ -10,11 +10,11 @@ defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.MapOfString do def type, do: :map def cast(object) do - with {status, %{} = data} when status in [:modified, :ok] <- MultiLanguage.validate_map(object) do + with {status, %{} = data} when status in [:modified, :ok] <- + MultiLanguage.validate_map(object) do {:ok, data} else {:modified, nil} -> {:ok, nil} - {:error, _} -> :error end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index fb8e36bbfd..f72102afcf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do import Ecto.Query import Pleroma.Web.ActivityPub.Utils import Pleroma.Web.ActivityPub.Visibility + import Pleroma.Web.Gettext import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2] require Logger @@ -1591,12 +1592,27 @@ def fetch_activities_bounded( |> Enum.reverse() end + defp validate_media_description_map(%{} = map) do + with {:ok, %{}} <- Pleroma.MultiLanguage.validate_map(map) do + :ok + else + _ -> :error + end + end + + defp validate_media_description_map(nil), do: :ok + defp validate_media_description_map(_), do: :error + @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} def upload(file, opts \\ []) do - with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do + with {_, :ok} <- {:description_map, validate_media_description_map(opts[:description_map])}, + {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do obj_data = Maps.put_if_present(data, "actor", opts[:actor]) Repo.insert(%Object{data: obj_data}) + else + {:description_map, _} -> {:error, dgettext("errors", "description_map invalid")} + e -> e end end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 1207c481bc..589f14f706 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -68,7 +68,7 @@ def create(user, params) do |> status() |> summary() |> with_valid(&attachments/1) - |> full_payload() + |> with_valid(&full_payload/1) |> expires_at() |> poll() |> with_valid(&in_reply_to/1) @@ -76,7 +76,7 @@ def create(user, params) do |> with_valid("e_post/1) |> with_valid(&visibility/1) |> with_valid("ing_visibility/1) - |> content() + |> with_valid(&content/1) |> with_valid(&to_and_cc/1) |> with_valid(&context/1) |> with_valid(&language/1) @@ -153,16 +153,24 @@ defp put_params(draft, params) do %__MODULE__{draft | params: params} end - defp status(%{params: %{status_map: status_map}} = draft) do - %__MODULE__{draft | status_map: status_map} + defp status(%{params: %{status_map: %{} = status_map}} = draft) do + with {:ok, %{}} <- MultiLanguage.validate_map(status_map) do + %__MODULE__{draft | status_map: status_map} + else + _ -> add_error(draft, dgettext("errors", "status_map is not a valid multilang map")) + end end defp status(%{params: %{status: status}} = draft) do %__MODULE__{draft | status: String.trim(status)} end - defp summary(%{params: %{spoiler_text_map: spoiler_text_map}} = draft) do - %__MODULE__{draft | summary_map: spoiler_text_map} + defp summary(%{params: %{spoiler_text_map: %{} = spoiler_text_map}} = draft) do + with {:ok, %{}} <- MultiLanguage.validate_map(spoiler_text_map) do + %__MODULE__{draft | summary_map: spoiler_text_map} + else + _ -> add_error(draft, dgettext("errors", "spoiler_text_map is not a valid multilang map")) + end end defp summary(%{params: params} = draft) do diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 5983ba9c6c..772bc44766 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -156,6 +156,7 @@ def make_poll_data(%{poll: %{options_map: options_map, expires_in: expires_in}} options = options |> Enum.uniq() with :ok <- validate_poll_expiration(expires_in, limits), + :ok <- validate_poll_options_map(options_map), :ok <- validate_poll_options_amount(options_map, limits), :ok <- validate_poll_options_length(options_map, limits) do {option_notes, emoji} = @@ -211,6 +212,20 @@ def make_poll_data(_data) do {:ok, {%{}, %{}}} end + defp validate_poll_options_map(options) do + if Enum.all?(options, fn opt -> + with {:ok, %{}} <- MultiLanguage.validate_map(opt) do + true + else + _ -> false + end + end) do + :ok + else + {:error, dgettext("errors", "Poll option map not valid")} + end + end + defp validate_poll_options_amount(options, %{max_options: max_options}) do cond do Enum.count(options) < 2 -> diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index fb67943396..9101fa1b67 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -34,6 +34,11 @@ def create( attachment_data = Map.put(object.data, "id", object.id) render(conn, "attachment.json", %{attachment: attachment_data}) + else + {:error, e} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: e}) end end @@ -57,6 +62,11 @@ def create2( conn |> put_status(202) |> render("attachment.json", %{attachment: attachment_data}) + else + {:error, e} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: e}) end end diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs index 7b886da6a6..39c44443da 100644 --- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -67,6 +67,26 @@ test "/api/v1/media, multilang", %{conn: conn, image: image} do assert object.data["actor"] == User.ap_id(conn.assigns[:user]) end + test "/api/v1/media, multilang, invalid description_map", %{conn: conn, image: image} do + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/media", %{ + "file" => image, + "description_map" => %{"a" => "mew", "b_" => "lol"} + }) + |> json_response_and_validate_schema(422) + end + + test "/api/v1/media, multilang, empty description_map", %{conn: conn, image: image} do + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/media", %{ + "file" => image, + "description_map" => %{} + }) + |> json_response_and_validate_schema(422) + end + test "/api/v2/media", %{conn: conn, user: user, image: image} do desc = "Description of the image" @@ -111,6 +131,26 @@ test "/api/v2/media, multilang", %{conn: conn, image: image} do assert object.data["actor"] == User.ap_id(conn.assigns[:user]) end + test "/api/v2/media, multilang, invalid description_map", %{conn: conn, image: image} do + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v2/media", %{ + "file" => image, + "description_map" => %{"a" => "mew", "b_" => "lol"} + }) + |> json_response_and_validate_schema(422) + end + + test "/api/v2/media, multilang, empty description_map", %{conn: conn, image: image} do + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v2/media", %{ + "file" => image, + "description_map" => %{} + }) + |> json_response_and_validate_schema(422) + end + test "/api/v2/media, upload_limit", %{conn: conn, user: user} do desc = "Description of the binary" @@ -133,7 +173,7 @@ test "/api/v2/media, upload_limit", %{conn: conn, user: user} do "file" => large_binary, "description" => desc }) - |> json_response_and_validate_schema(400) + |> json_response_and_validate_schema(422) end) =~ "[error] Elixir.Pleroma.Upload store (using Pleroma.Uploaders.Local) failed: :file_too_large" diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index b6966fb051..aac7d938ca 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -164,6 +164,78 @@ test "posting a multilang status", %{conn: conn} do assert Activity.get_by_id(id) end + test "posting a multilang status, invalid language code in status_map", %{conn: conn} do + idempotency_key = "Pikachu rocks!" + + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status_map" => %{"a" => "mew mew", "b_" => "lol lol"}, + "spoiler_text_map" => %{"a" => "mew", "b" => "lol"}, + "sensitive" => "0" + }) + + assert %{ + "error" => _ + } = json_response_and_validate_schema(conn_one, 422) + end + + test "posting a multilang status, empty status_map", %{conn: conn} do + idempotency_key = "Pikachu rocks!" + + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status_map" => %{}, + "spoiler_text_map" => %{"a" => "mew", "b" => "lol"}, + "sensitive" => "0" + }) + + assert %{ + "error" => _ + } = json_response_and_validate_schema(conn_one, 422) + end + + test "posting a multilang status, invalid language code in spoiler_text_map", %{conn: conn} do + idempotency_key = "Pikachu rocks!" + + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status_map" => %{"a" => "mew mew", "b" => "lol lol"}, + "spoiler_text_map" => %{"a" => "mew", "b_" => "lol"}, + "sensitive" => "0" + }) + + assert %{ + "error" => _ + } = json_response_and_validate_schema(conn_one, 422) + end + + test "posting a multilang status, empty spoiler_text_map", %{conn: conn} do + idempotency_key = "Pikachu rocks!" + + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> put_req_header("idempotency-key", idempotency_key) + |> post("/api/v1/statuses", %{ + "status_map" => %{"a" => "mew mew", "b" => "lol lol"}, + "spoiler_text_map" => %{}, + "sensitive" => "0" + }) + + assert %{ + "error" => _ + } = json_response_and_validate_schema(conn_one, 422) + end + test "posting a multilang status with singlelang summary", %{conn: conn} do idempotency_key = "Pikachu rocks!" @@ -713,6 +785,44 @@ test "posting a multilang poll", %{conn: conn} do assert question.data["closed"] =~ "Z" end + test "posting a multilang poll, invalid lang code", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "Who is the #bestgrill?", + "poll" => %{ + "options_map" => [ + %{"a" => "Rei", "b" => "1"}, + %{"a" => "Asuka", "b_" => "2"}, + %{"a" => "Misato", "b" => "3"} + ], + "expires_in" => 420 + } + }) + + assert %{"error" => _} = json_response_and_validate_schema(conn, 422) + end + + test "posting a multilang poll, empty map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "Who is the #bestgrill?", + "poll" => %{ + "options_map" => [ + %{"a" => "Rei", "b" => "1"}, + %{}, + %{"a" => "Misato", "b" => "3"} + ], + "expires_in" => 420 + } + }) + + assert %{"error" => _} = json_response_and_validate_schema(conn, 422) + end + test "option limit is enforced", %{conn: conn} do limit = Config.get([:instance, :poll_limits, :max_options])