Merge branch '210_twitter_api_uploads_alt_text' into 'develop'
authorkaniini <nenolod@gmail.com>
Thu, 6 Dec 2018 07:36:21 +0000 (07:36 +0000)
committerkaniini <nenolod@gmail.com>
Thu, 6 Dec 2018 07:36:21 +0000 (07:36 +0000)
[#210] TwitterAPI: alt text support for uploaded images. Mastodon API uploads security fix.

See merge request pleroma/pleroma!496

lib/pleroma/object.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
test/support/data_case.ex
test/upload_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/twitter_api/twitter_api_controller_test.exs
test/web/twitter_api/twitter_api_test.exs

index 03a75dfbdbbcdcf00bbf9493b1c8cc9272868266..31c8dd5bd13cba9913d322729b34621999da9d98 100644 (file)
@@ -1,6 +1,6 @@
 defmodule Pleroma.Object do
   use Ecto.Schema
-  alias Pleroma.{Repo, Object, Activity}
+  alias Pleroma.{Repo, Object, User, Activity}
   import Ecto.{Query, Changeset}
 
   schema "objects" do
@@ -31,6 +31,13 @@ defmodule Pleroma.Object do
   def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
   def normalize(_), do: nil
 
+  # Owned objects can only be mutated by their owner
+  def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
+    do: actor == ap_id
+
+  # Legacy objects can be mutated by anybody
+  def authorize_mutation(%Object{}, %User{}), do: true
+
   if Mix.env() == :test do
     def get_cached_by_ap_id(ap_id) do
       get_by_ap_id(ap_id)
index 60253a715909edc9f781ba22d9957db629388c67..28da57a101d46a72ee5e62376c1ae8b1f0de114a 100644 (file)
@@ -574,7 +574,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   def upload(file, opts \\ []) do
     with {:ok, data} <- Upload.store(file, opts) do
-      Repo.insert(%Object{data: data})
+      obj_data =
+        if opts[:actor] do
+          Map.put(data, "actor", opts[:actor])
+        else
+          data
+        end
+
+      Repo.insert(%Object{data: obj_data})
     end
   end
 
index 15373dd901f27b7e6d9ba660380f7ba60035f41c..eecfc742b2096cd3b0395684401cb6b5d2d41d04 100644 (file)
@@ -433,33 +433,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     |> json([])
   end
 
-  def update_media(%{assigns: %{user: _}} = conn, data) do
+  def update_media(%{assigns: %{user: user}} = conn, data) do
     with %Object{} = object <- Repo.get(Object, data["id"]),
+         true <- Object.authorize_mutation(object, user),
          true <- is_binary(data["description"]),
          description <- data["description"] do
       new_data = %{object.data | "name" => description}
 
-      change = Object.change(object, %{data: new_data})
-      {:ok, _} = Repo.update(change)
+      {:ok, _} =
+        object
+        |> Object.change(%{data: new_data})
+        |> Repo.update()
 
-      data =
-        new_data
-        |> Map.put("id", object.id)
-
-      render(conn, StatusView, "attachment.json", %{attachment: data})
+      attachment_data = Map.put(new_data, "id", object.id)
+      render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
     end
   end
 
-  def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
-    with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do
-      change = Object.change(object, %{data: object.data})
-      {:ok, object} = Repo.update(change)
-
-      objdata =
-        object.data
-        |> Map.put("id", object.id)
-
-      render(conn, StatusView, "attachment.json", %{attachment: objdata})
+  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+    with {:ok, object} <-
+           ActivityPub.upload(file,
+             actor: User.ap_id(user),
+             description: Map.get(data, "description")
+           ) do
+      attachment_data = Map.put(object.data, "id", object.id)
+      render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
     end
   end
 
index d6a9d57798882186235eb00d9f317740be970351..b7c79d2eb06d1668b8b2fa54937b64371e5853b4 100644 (file)
@@ -324,6 +324,7 @@ defmodule Pleroma.Web.Router do
 
     post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
     post("/media/upload", TwitterAPI.Controller, :upload_json)
+    post("/media/metadata/create", TwitterAPI.Controller, :update_media)
 
     post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
     post("/favorites/create", TwitterAPI.Controller, :favorite)
index c19a4f0849d6ce85862cdfce1f1d5cc48a886058..9c485d965592b8d2b587414b3637848218a109c7 100644 (file)
@@ -93,8 +93,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     end
   end
 
-  def upload(%Plug.Upload{} = file, format \\ "xml") do
-    {:ok, object} = ActivityPub.upload(file)
+  def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
+    {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
 
     url = List.first(object.data["url"])
     href = url["href"]
index 8fd6ea0783538c9867382c8f29f705f28b5a2518..0ccf937b0f003b677797bc83fcdba8019e070c4c 100644 (file)
@@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
-  alias Pleroma.{Repo, Activity, User, Notification}
+  alias Pleroma.{Repo, Activity, Object, User, Notification}
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Ecto.Changeset
@@ -226,16 +226,51 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     end
   end
 
-  def upload(conn, %{"media" => media}) do
-    response = TwitterAPI.upload(media)
+  @doc """
+  Updates metadata of uploaded media object.
+  Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
+  """
+  def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
+    object = Repo.get(Object, id)
+    description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
+
+    {conn, status, response_body} =
+      cond do
+        !object ->
+          {halt(conn), :not_found, ""}
+
+        !Object.authorize_mutation(object, user) ->
+          {halt(conn), :forbidden, "You can only update your own uploads."}
+
+        !is_binary(description) ->
+          {conn, :not_modified, ""}
+
+        true ->
+          new_data = Map.put(object.data, "name", description)
+
+          {:ok, _} =
+            object
+            |> Object.change(%{data: new_data})
+            |> Repo.update()
+
+          {conn, :no_content, ""}
+      end
+
+    conn
+    |> put_status(status)
+    |> json(response_body)
+  end
+
+  def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+    response = TwitterAPI.upload(media, user)
 
     conn
     |> put_resp_content_type("application/atom+xml")
     |> send_resp(200, response)
   end
 
-  def upload_json(conn, %{"media" => media}) do
-    response = TwitterAPI.upload(media, "json")
+  def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+    response = TwitterAPI.upload(media, user, "json")
 
     conn
     |> json_reply(200, response)
index 8eff0fd946185470784959f3aa7a9b03695d1db4..9dde6b5e552160b7e9f137b283df845724900e72 100644 (file)
@@ -36,6 +36,23 @@ defmodule Pleroma.DataCase do
     :ok
   end
 
+  def ensure_local_uploader(_context) do
+    uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+    filters = Pleroma.Config.get([Pleroma.Upload, :filters])
+
+    unless uploader == Pleroma.Uploaders.Local || filters != [] do
+      Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+      Pleroma.Config.put([Pleroma.Upload, :filters], [])
+
+      on_exit(fn ->
+        Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
+        Pleroma.Config.put([Pleroma.Upload, :filters], filters)
+      end)
+    end
+
+    :ok
+  end
+
   @doc """
   A helper that transform changeset errors to a map of messages.
 
index b2ce755d2b44fa42ec2f6cf33ab1aa55f410966f..f2cad4cf0cccaee9489ce88fdd5323610dc7198a 100644 (file)
@@ -3,22 +3,7 @@ defmodule Pleroma.UploadTest do
   use Pleroma.DataCase
 
   describe "Storing a file with the Local uploader" do
-    setup do
-      uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
-      filters = Pleroma.Config.get([Pleroma.Upload, :filters])
-
-      unless uploader == Pleroma.Uploaders.Local || filters != [] do
-        Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
-        Pleroma.Config.put([Pleroma.Upload, :filters], [])
-
-        on_exit(fn ->
-          Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
-          Pleroma.Config.put([Pleroma.Upload, :filters], filters)
-        end)
-      end
-
-      :ok
-    end
+    setup [:ensure_local_uploader]
 
     test "returns a media url" do
       File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
index 0ff6116482ff11eaf263e917e8b3992e499c913f..092f0c9fc9887032b58f9cc3a62f124d3c52318d 100644 (file)
@@ -2,7 +2,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
   use Pleroma.Web.ConnCase
 
   alias Pleroma.Web.TwitterAPI.TwitterAPI
-  alias Pleroma.{Repo, User, Activity, Notification}
+  alias Pleroma.{Repo, User, Object, Activity, Notification}
   alias Pleroma.Web.{OStatus, CommonAPI}
   alias Pleroma.Web.ActivityPub.ActivityPub
 
@@ -810,7 +810,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       }
 
       media =
-        TwitterAPI.upload(file, "json")
+        TwitterAPI.upload(file, user, "json")
         |> Poison.decode!()
 
       {:ok, image_post} =
@@ -965,6 +965,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
     assert media["type"] == "image"
     assert media["description"] == desc
+    assert media["id"]
+
+    object = Repo.get(Object, media["id"])
+    assert object.data["actor"] == User.ap_id(user)
   end
 
   test "hashtag timeline", %{conn: conn} do
index a8a9da781a5bf1952f15e1ef52dfa82942874fe7..4119d1dd8142b03ffe996f3c072b67b66e27bba9 100644 (file)
@@ -1376,4 +1376,82 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
       assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end)
     end
   end
+
+  describe "POST /api/media/upload" do
+    setup context do
+      Pleroma.DataCase.ensure_local_uploader(context)
+    end
+
+    test "it performs the upload and sets `data[actor]` with AP id of uploader user", %{
+      conn: conn
+    } do
+      user = insert(:user)
+
+      upload_filename = "test/fixtures/image_tmp.jpg"
+      File.cp!("test/fixtures/image.jpg", upload_filename)
+
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname(upload_filename),
+        filename: "image.jpg"
+      }
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> put_req_header("content-type", "application/octet-stream")
+        |> post("/api/media/upload", %{
+          "media" => file
+        })
+        |> json_response(:ok)
+
+      assert response["media_id"]
+      object = Repo.get(Object, response["media_id"])
+      assert object
+      assert object.data["actor"] == User.ap_id(user)
+    end
+  end
+
+  describe "POST /api/media/metadata/create" do
+    setup do
+      object = insert(:note)
+      user = User.get_by_ap_id(object.data["actor"])
+      %{object: object, user: user}
+    end
+
+    test "it returns :forbidden status on attempt to modify someone else's upload", %{
+      conn: conn,
+      object: object
+    } do
+      initial_description = object.data["name"]
+      another_user = insert(:user)
+
+      conn
+      |> assign(:user, another_user)
+      |> post("/api/media/metadata/create", %{"media_id" => object.id})
+      |> json_response(:forbidden)
+
+      object = Repo.get(Object, object.id)
+      assert object.data["name"] == initial_description
+    end
+
+    test "it updates `data[name]` of referenced Object with provided value", %{
+      conn: conn,
+      object: object,
+      user: user
+    } do
+      description = "Informative description of the image. Initial value: #{object.data["name"]}}"
+
+      conn
+      |> assign(:user, user)
+      |> post("/api/media/metadata/create", %{
+        "media_id" => object.id,
+        "alt_text" => %{"text" => description}
+      })
+      |> json_response(:no_content)
+
+      object = Repo.get(Object, object.id)
+      assert object.data["name"] == description
+    end
+  end
 end
index 76de68783260dcaa912eaf911989b9508f2d52c1..05f832de09ff490b60065d760b17a72c7eb6c1d4 100644 (file)
@@ -182,13 +182,15 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
   end
 
   test "upload a file" do
+    user = insert(:user)
+
     file = %Plug.Upload{
       content_type: "image/jpg",
       path: Path.absname("test/fixtures/image.jpg"),
       filename: "an_image.jpg"
     }
 
-    response = TwitterAPI.upload(file)
+    response = TwitterAPI.upload(file, user)
 
     assert is_binary(response)
   end