Merge branch '210_twitter_api_uploads_alt_text' into 'develop'
[#210] TwitterAPI: alt text support for uploaded images. Mastodon API uploads security fix. See merge request pleroma/pleroma!496
This commit is contained in:
commit
ccf0b46dd6
11 changed files with 180 additions and 46 deletions
|
@ -1,6 +1,6 @@
|
||||||
defmodule Pleroma.Object do
|
defmodule Pleroma.Object do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
alias Pleroma.{Repo, Object, Activity}
|
alias Pleroma.{Repo, Object, User, Activity}
|
||||||
import Ecto.{Query, Changeset}
|
import Ecto.{Query, Changeset}
|
||||||
|
|
||||||
schema "objects" do
|
schema "objects" do
|
||||||
|
@ -31,6 +31,13 @@ def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"])
|
||||||
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
|
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
|
||||||
def normalize(_), do: nil
|
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
|
if Mix.env() == :test do
|
||||||
def get_cached_by_ap_id(ap_id) do
|
def get_cached_by_ap_id(ap_id) do
|
||||||
get_by_ap_id(ap_id)
|
get_by_ap_id(ap_id)
|
||||||
|
|
|
@ -574,7 +574,14 @@ def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
|
||||||
|
|
||||||
def upload(file, opts \\ []) do
|
def upload(file, opts \\ []) do
|
||||||
with {:ok, data} <- Upload.store(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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -433,33 +433,31 @@ def relationships(%{assigns: %{user: user}} = conn, _) do
|
||||||
|> json([])
|
|> json([])
|
||||||
end
|
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"]),
|
with %Object{} = object <- Repo.get(Object, data["id"]),
|
||||||
|
true <- Object.authorize_mutation(object, user),
|
||||||
true <- is_binary(data["description"]),
|
true <- is_binary(data["description"]),
|
||||||
description <- data["description"] do
|
description <- data["description"] do
|
||||||
new_data = %{object.data | "name" => description}
|
new_data = %{object.data | "name" => description}
|
||||||
|
|
||||||
change = Object.change(object, %{data: new_data})
|
{:ok, _} =
|
||||||
{:ok, _} = Repo.update(change)
|
object
|
||||||
|
|> Object.change(%{data: new_data})
|
||||||
|
|> Repo.update()
|
||||||
|
|
||||||
data =
|
attachment_data = Map.put(new_data, "id", object.id)
|
||||||
new_data
|
render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
|
||||||
|> Map.put("id", object.id)
|
|
||||||
|
|
||||||
render(conn, StatusView, "attachment.json", %{attachment: data})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
|
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
|
||||||
with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do
|
with {:ok, object} <-
|
||||||
change = Object.change(object, %{data: object.data})
|
ActivityPub.upload(file,
|
||||||
{:ok, object} = Repo.update(change)
|
actor: User.ap_id(user),
|
||||||
|
description: Map.get(data, "description")
|
||||||
objdata =
|
) do
|
||||||
object.data
|
attachment_data = Map.put(object.data, "id", object.id)
|
||||||
|> Map.put("id", object.id)
|
render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
|
||||||
|
|
||||||
render(conn, StatusView, "attachment.json", %{attachment: objdata})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -324,6 +324,7 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
|
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
|
||||||
post("/media/upload", TwitterAPI.Controller, :upload_json)
|
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/:id", TwitterAPI.Controller, :favorite)
|
||||||
post("/favorites/create", TwitterAPI.Controller, :favorite)
|
post("/favorites/create", TwitterAPI.Controller, :favorite)
|
||||||
|
|
|
@ -93,8 +93,8 @@ def unfav(%User{} = user, ap_id_or_id) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload(%Plug.Upload{} = file, format \\ "xml") do
|
def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
|
||||||
{:ok, object} = ActivityPub.upload(file)
|
{:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
|
||||||
|
|
||||||
url = List.first(object.data["url"])
|
url = List.first(object.data["url"])
|
||||||
href = url["href"]
|
href = url["href"]
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|
||||||
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
|
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
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.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
|
@ -226,16 +226,51 @@ def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload(conn, %{"media" => media}) do
|
@doc """
|
||||||
response = TwitterAPI.upload(media)
|
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
|
conn
|
||||||
|> put_resp_content_type("application/atom+xml")
|
|> put_resp_content_type("application/atom+xml")
|
||||||
|> send_resp(200, response)
|
|> send_resp(200, response)
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload_json(conn, %{"media" => media}) do
|
def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
|
||||||
response = TwitterAPI.upload(media, "json")
|
response = TwitterAPI.upload(media, user, "json")
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> json_reply(200, response)
|
|> json_reply(200, response)
|
||||||
|
|
|
@ -36,6 +36,23 @@ defmodule Pleroma.DataCase do
|
||||||
:ok
|
:ok
|
||||||
end
|
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 """
|
@doc """
|
||||||
A helper that transform changeset errors to a map of messages.
|
A helper that transform changeset errors to a map of messages.
|
||||||
|
|
||||||
|
|
|
@ -3,22 +3,7 @@ defmodule Pleroma.UploadTest do
|
||||||
use Pleroma.DataCase
|
use Pleroma.DataCase
|
||||||
|
|
||||||
describe "Storing a file with the Local uploader" do
|
describe "Storing a file with the Local uploader" do
|
||||||
setup do
|
setup [:ensure_local_uploader]
|
||||||
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
|
|
||||||
|
|
||||||
test "returns a media url" do
|
test "returns a media url" do
|
||||||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
||||||
|
|
|
@ -2,7 +2,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
||||||
use Pleroma.Web.ConnCase
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
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.{OStatus, CommonAPI}
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
|
@ -810,7 +810,7 @@ test "gets an users media", %{conn: conn} do
|
||||||
}
|
}
|
||||||
|
|
||||||
media =
|
media =
|
||||||
TwitterAPI.upload(file, "json")
|
TwitterAPI.upload(file, user, "json")
|
||||||
|> Poison.decode!()
|
|> Poison.decode!()
|
||||||
|
|
||||||
{:ok, image_post} =
|
{:ok, image_post} =
|
||||||
|
@ -965,6 +965,10 @@ test "media upload", %{conn: conn} do
|
||||||
|
|
||||||
assert media["type"] == "image"
|
assert media["type"] == "image"
|
||||||
assert media["description"] == desc
|
assert media["description"] == desc
|
||||||
|
assert media["id"]
|
||||||
|
|
||||||
|
object = Repo.get(Object, media["id"])
|
||||||
|
assert object.data["actor"] == User.ap_id(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "hashtag timeline", %{conn: conn} do
|
test "hashtag timeline", %{conn: conn} do
|
||||||
|
|
|
@ -1376,4 +1376,82 @@ test "it returns users, ordered by similarity", %{conn: conn} do
|
||||||
assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end)
|
assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -182,13 +182,15 @@ test "Unblock another user using screen_name" do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upload a file" do
|
test "upload a file" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
file = %Plug.Upload{
|
file = %Plug.Upload{
|
||||||
content_type: "image/jpg",
|
content_type: "image/jpg",
|
||||||
path: Path.absname("test/fixtures/image.jpg"),
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
filename: "an_image.jpg"
|
filename: "an_image.jpg"
|
||||||
}
|
}
|
||||||
|
|
||||||
response = TwitterAPI.upload(file)
|
response = TwitterAPI.upload(file, user)
|
||||||
|
|
||||||
assert is_binary(response)
|
assert is_binary(response)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue