defmodule BallsPDS.Router do require Logger use Plug.Router alias BallsPDS.Ecto.Schema.Object alias BallsPDS.Ecto.Schema.ObjectReadAgent alias BallsPDS.Util.ACL alias BallsPDS.Plug.WACAuthenticationPlug alias BallsPDS.Plug.ObjectPlug alias BallsPDS.Plug.ObjectAuthorizationPlug alias BallsPDS.Repo alias BallsPDS.Ecto.Schema.CollectionObject import BallsPDS.Plug.ObjectPlug, only: [is_acl_get?: 1] plug(:match) plug(:dispatch) plug(Plug.Parsers, parsers: [:multipart], pass: ["*/*"], length: 100_000_000 ) plug(WACAuthenticationPlug) plug(ObjectPlug) plug(ObjectAuthorizationPlug) match _, via: :get do if is_acl_get?(conn) do handle_acl_get(conn) else handle_authorized_get(conn) end end match _, via: :post do handle_authorized_post(conn) end match _ do send_resp(conn, 405, "Method not allowed") end def handle_acl_get(conn) do object_path = String.replace_suffix(conn.request_path, ".acl.jsonld", "") case Object.get_by_path(object_path) do nil -> send_resp(conn, 404, "Not found") object -> authorized_readers = ObjectReadAgent.get_read_agents(object) document = ACL.render_acl_document(object_path, authorized_readers) conn |> put_resp_content_type("application/ld+json") |> send_resp(200, Jason.encode!(document)) end end def handle_authorized_get(conn) do object = %{ :content_type => content_type, :storage_key => storage_key, :activitypub_type => type } = conn.assigns[:object] case File.read("./priv/data/#{storage_key}") do {:ok, file_contents} -> is_collection? = type in ["Collection", "OrderedCollection"] file_contents = if is_collection? do # Inject back in the collection items. collection = Jason.decode!(file_contents) # TODO: not sure how to handle every weird way a collection might be represented not just list of ids. ids = CollectionObject.get_ids(object) |> Enum.map(fn {_, ap_id} -> ap_id end) items_name = if type == "OrderedCollection" do "orderedItems" else "items" end Map.put(collection, items_name, ids) |> Map.put("totalItems", length(ids)) Jason.encode(collection) else file_contents end conn |> put_resp_content_type(content_type) |> send_resp(200, file_contents) {:error, _error} -> send_resp(conn, 500, "Unknown error") end end defp extract_collection_info(%{"type" => "OrderedCollection", "orderedItems" => items}) when is_list(items) do ids = Enum.reduce(items, [], fn item, acc -> case dereference_id(item) do nil -> acc id -> [id | acc] end end) |> Enum.reverse() {"OrderedCollection", ids} end defp extract_collection_info(%{"type" => "Collection", "items" => items}) when is_list(items) do ids = Enum.reduce(items, [], fn item, acc -> case dereference_id(item) do nil -> acc id -> [id | acc] end end) |> Enum.reverse() {"Collection", ids} end defp extract_collection_info(_), do: {nil, nil} defp dereference_id(object) when is_binary(object), do: object defp dereference_id(%{"id" => id}) when is_binary(id), do: id defp dereference_id(_), do: nil def handle_authorized_post(%Plug.Conn{:halted => false, :method => "POST"} = conn) do conn |> maybe_set_public() |> maybe_set_content_type() |> validate_read_acls() |> maybe_write_object() |> maybe_save_db() |> maybe_set_read_acls() |> maybe_save_collection_items() |> send_resp(200, "OK") |> halt() end defp maybe_set_public(%Plug.Conn{:halted => false} = conn) do with {:object, object} when not is_nil(object) <- {:object, conn.assigns[:object]}, object_params <- conn.assigns[:object_params] || %{}, {:public, %{"public" => public?}} when public? in ["true", "false"] <- {:public, conn.body_params}, public? <- public? == "true" do Map.put(object_params, :public, public?) Plug.Conn.assign(conn, :object_params, object_params) else {:object, nil} -> conn {:public, _} -> send_resp(conn, 500, "Set public error") |> halt() end end defp maybe_set_public(%Plug.Conn{} = conn), do: conn defp maybe_set_content_type(%Plug.Conn{:halted => false} = conn) do with {:object, object} when not is_nil(object) <- {:object, conn.assigns[:object]}, object_params <- conn.assigns[:object_params] || %{} do object_params = case conn.body_params do %{"file" => %{"content_type" => content_type}} -> Map.put(object_params, :content_type, content_type) %{"content_type" => content_type} when is_binary(content_type) -> Map.put(object_params, :content_type, content_type) _ -> object_params end Plug.Conn.assign(conn, :object_params, object_params) else {:object, nil} -> conn end end defp maybe_set_content_type(%Plug.Conn{} = conn), do: conn defp validate_read_acls(%Plug.Conn{:halted => false} = conn) do read = case Map.get(conn.body_params, "read") do val when is_binary(val) -> [val] val when is_list(val) -> val val -> val end case read do nil -> conn read when is_list(read) -> all_valid? = Enum.reduce_while(read, true, fn acl, _ -> if ACL.is_valid_acl?(acl) do {:cont, true} else {:halt, false} end end) if all_valid? do conn else send_resp(conn, 500, "Validate read error: invalid acl") |> halt() end _ -> send_resp(conn, 500, "Validate read error") |> halt() end end defp validate_read_acls(%Plug.Conn{} = conn), do: conn # This can't be called until object is already inserted. Assume already validated. defp maybe_set_read_acls(%Plug.Conn{:halted => false} = conn) do read = case Map.get(conn.body_params, "read") do val when is_binary(val) -> [val] val when is_list(val) -> val end with {:object, %Object{} = object} <- {:object, conn.assigns[:object]}, {:reset_read, _} <- {:reset_read, ObjectReadAgent.revoke_all_read(object)}, {:update_read, :ok} <- {:update_read, ObjectReadAgent.authorize_read(object, read)} do conn else {:object, nil} -> conn {:update_read, error} -> Logger.error("Failed to authorized read: #{inspect(error)}") send_resp(conn, 500, "Set read error") |> halt() end end defp maybe_set_read_acls(%Plug.Conn{} = conn), do: conn defp maybe_write_object( %Plug.Conn{ :halted => false, :request_path => request_path, :body_params => %{ "file" => %{"path" => uploaded_file_path, "content_type" => content_type} } } = conn ) do with {:read, {:ok, body}} <- {:read, File.read(uploaded_file_path)}, {:parse, {:ok, %{:body => body, :activitypub_type => activitypub_type, :ids => ap_collection_ids}}} <- {:parse, maybe_parse_ap_object(body, content_type)}, storage_key <- request_path |> Base.encode16(case: :lower), filename <- "./priv/data/#{storage_key}", {:write, :ok} <- {:write, File.write(filename, body)}, object_params <- conn.assigns[:object_params], object_params <- Map.put(object_params, :storage_key, storage_key), conn <- Plug.Conn.assign(conn, :object_params, object_params) do conn = if activitypub_type == nil do conn else object_params = Map.put(object_params, :activitypub_type, activitypub_type) Plug.Conn.assign(conn, :object_params, object_params) end conn = if is_list(ap_collection_ids) and length(ap_collection_ids) > 0 do conn |> assign(:ids, ap_collection_ids) else conn end conn else {:read, error} -> Logger.error("Failed to read uploaded file: #{inspect(error)}") send_resp(conn, 500, "Error") |> halt() {:write, error} -> Logger.error("Failed to write uploaded file to location: #{inspect(error)}") send_resp(conn, 500, "Error") |> halt() end end defp maybe_write_object(%Plug.Conn{} = conn), do: conn defp maybe_parse_ap_object(body, content_type) when is_binary(body) and is_binary(content_type) do if String.starts_with?(content_type, "application/json") do with {:parse, {:ok, %{"@context" => _, "type" => activitypub_type, "id" => activitypub_id} = parsed}} when is_binary(activitypub_id) and is_binary(activitypub_type) <- {:parse, Jason.decode(body)}, {:collection, {_, ids}} <- {:collection, extract_collection_info(parsed)} do parsed = if activitypub_type in ["Collection", "OrderedCollection"] do parsed |> Map.drop(["items", "orderedItems"]) else parsed end {:ok, %{ body: Jason.encode(parsed), activitypub_type: activitypub_type, ids: ids }} else {:parse, {:ok, _parsed}} -> {:ok, %{ body: body, activitypub_type: nil, ids: nil }} {:parse, {:error, error}} -> # Should I really save broken JSON? {:error, error} end else {:ok, %{ body: body, activitypub_type: nil, ids: nil }} end end defp maybe_save_db(%Plug.Conn{:halted => false} = conn) do with {:object, %Object{} = object} <- {:object, conn.assigns[:object]}, {:params, %{} = object_params} when map_size(object_params) > 0 <- {:params, conn.assigns[:object_params]}, {:changeset, {:ok, changeset}} <- {:changeset, Object.changeset(object, object_params)}, {:upsert, {:ok, object}} <- {:upsert, Repo.insert(changeset, on_conflict: :replace_all)} do conn |> assign(:object, object) |> assign(:object_params, %{}) else {:object, nil} -> Logger.error("This should not happen, object was nil in save to db.") send_resp(conn, 500, "Error") |> halt() {:params, _} -> conn {:changeset, {:error, error}} -> Logger.error("Changeset on save to db failed for some reason: #{inspect(error)}") send_resp(conn, 500, "Error") |> halt() {:upsert, {:error, error}} -> Logger.error("Upsert failed for some reason: #{inspect(error)}") send_resp(conn, 500, "Error") |> halt() end end defp maybe_save_db(%Plug.Conn{} = conn), do: conn defp maybe_save_collection_items( %Plug.Conn{:halted => false, :assigns => %{:ids => ids, :object => object}} = conn ) do CollectionObject.delete_all(object) CollectionObject.insert_all(object, wrap(ids)) conn end defp maybe_save_collection_items( %Plug.Conn{ :halted => false, :assigns => %{:object => object}, :body_params => %{"ids" => ids} } = conn ) do CollectionObject.delete_all(object) CollectionObject.insert_all(object, wrap(ids)) conn end defp maybe_save_collection_items( %Plug.Conn{ :halted => false, :assigns => %{:object => object}, :body_params => %{"append_ids" => ids} } = conn ) do CollectionObject.insert_all(object, wrap(ids)) conn end defp maybe_save_collection_items( %Plug.Conn{ :halted => false, :assigns => %{:object => object}, :body_params => %{"delete_ids" => ids} } = conn ) do CollectionObject.delete_all(object, wrap(ids)) conn end defp maybe_save_collection_items(%Plug.Conn{} = conn), do: conn defp wrap(id) when is_binary(id), do: [id] defp wrap(ids) when is_list(ids), do: ids end