diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7a372e11..1ec6c11cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,12 @@ switched to a new configuration mechanism, however it was not officially removed
### Added
- Rich media failure tracking (along with `:failure_backoff` option).
+
+ Admin API Changes
+
+- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API
+
+
### Fixed
- Default HTTP adapter not respecting pool setting, leading to possible OOM.
- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index bc96abbf0..eba92dd1f 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -1455,3 +1455,45 @@ Loads json generated from `config/descriptions.exs`.
"unread": false
}
```
+
+## `GET /api/pleroma/admin/instance_document/:document_name`
+
+### Gets an instance document
+
+- Authentication: required
+
+- Response:
+
+``` json
+{
+ "url": "https://example.com/instance/panel.html"
+}
+```
+
+## `PATCH /api/pleroma/admin/instance_document/:document_name`
+- Params:
+ - `file` (the file to be uploaded, using multipart form data.)
+
+### Updates an instance document
+
+- Authentication: required
+
+- Response:
+
+``` json
+{
+ "url": "https://example.com/instance/panel.html"
+}
+```
+
+## `DELETE /api/pleroma/admin/instance_document/:document_name`
+
+### Delete an instance document
+
+- Response:
+
+``` json
+{
+ "url": "https://example.com/instance/panel.html"
+}
+```
diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
new file mode 100644
index 000000000..2144e44ac
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.InstanceDocument
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
+
+ plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
+ plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
+
+ def show(conn, %{name: document_name}) do
+ with {:ok, url} <- InstanceDocument.get(document_name) do
+ json(conn, %{"url" => url})
+ end
+ end
+
+ def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
+ with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
+ json(conn, %{"url" => url})
+ end
+ end
+
+ def delete(conn, %{name: document_name}) do
+ with :ok <- InstanceDocument.delete(document_name) do
+ json(conn, %{})
+ end
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
new file mode 100644
index 000000000..e0eb993fb
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/instance_document_operation.ex
@@ -0,0 +1,108 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Helpers
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Admin", "InstanceDocument"],
+ summary: "Get the instance document",
+ operationId: "AdminAPI.InstanceDocumentController.show",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: [
+ Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
+ required: true
+ )
+ | Helpers.admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("InstanceDocument", "application/json", instance_document()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Admin", "InstanceDocument"],
+ summary: "Update the instance document",
+ operationId: "AdminAPI.InstanceDocumentController.update",
+ security: [%{"oAuth" => ["write"]}],
+ requestBody: Helpers.request_body("Parameters", update_request()),
+ parameters: [
+ Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
+ required: true
+ )
+ | Helpers.admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("InstanceDocument", "application/json", instance_document()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "UpdateRequest",
+ description: "POST body for uploading the file",
+ type: :object,
+ required: [:file],
+ properties: %{
+ file: %Schema{
+ type: :string,
+ format: :binary,
+ description: "The file to be uploaded, using multipart form data."
+ }
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Admin", "InstanceDocument"],
+ summary: "Get the instance document",
+ operationId: "AdminAPI.InstanceDocumentController.delete",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [
+ Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
+ required: true
+ )
+ | Helpers.admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("InstanceDocument", "application/json", instance_document()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp instance_document do
+ %Schema{
+ title: "InstanceDocument",
+ type: :object,
+ properties: %{
+ url: %Schema{type: :string}
+ },
+ example: %{
+ "url" => "https://example.com/static/terms-of-service.html"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex
new file mode 100644
index 000000000..969a44e41
--- /dev/null
+++ b/lib/pleroma/web/instance_document.ex
@@ -0,0 +1,62 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.InstanceDocument do
+ alias Pleroma.Config
+ alias Pleroma.Web.Endpoint
+
+ @instance_documents %{
+ "terms-of-service" => "/static/terms-of-service.html",
+ "instance-panel" => "/instance/panel.html"
+ }
+
+ @spec get(String.t()) :: {:ok, String.t()} | {:error, atom()}
+ def get(document_name) do
+ case Map.fetch(@instance_documents, document_name) do
+ {:ok, path} -> {:ok, Path.join(Endpoint.url(), path)}
+ _ -> {:error, :not_found}
+ end
+ end
+
+ @spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
+ def put(document_name, origin_path) do
+ with {_, {:ok, destination_path}} <-
+ {:instance_document, Map.fetch(@instance_documents, document_name)},
+ :ok <- put_file(origin_path, destination_path) do
+ {:ok, Path.join(Endpoint.url(), destination_path)}
+ else
+ {:instance_document, :error} -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ @spec delete(String.t()) :: :ok | {:error, atom()}
+ def delete(document_name) do
+ with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)},
+ instance_static_dir_path <- instance_static_dir(path),
+ :ok <- File.rm(instance_static_dir_path) do
+ :ok
+ else
+ {:instance_document, :error} -> {:error, :not_found}
+ {:error, :enoent} -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ defp put_file(origin_path, destination_path) do
+ with destination <- instance_static_dir(destination_path),
+ {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
+ {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
+ :ok
+ else
+ {error, _} -> {:error, error}
+ end
+ end
+
+ defp instance_static_dir(filename) do
+ [:instance, :static_dir]
+ |> Config.get!()
+ |> Path.join(filename)
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index e4440d442..a4a58c2c4 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -182,6 +182,10 @@ defmodule Pleroma.Web.Router do
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
+ get("/instance_document/:name", InstanceDocumentController, :show)
+ patch("/instance_document/:name", InstanceDocumentController, :update)
+ delete("/instance_document/:name", InstanceDocumentController, :delete)
+
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
diff --git a/test/fixtures/custom_instance_panel.html b/test/fixtures/custom_instance_panel.html
new file mode 100644
index 000000000..6371a1e43
--- /dev/null
+++ b/test/fixtures/custom_instance_panel.html
@@ -0,0 +1 @@
+
Custom instance panel
\ No newline at end of file
diff --git a/test/web/admin_api/controllers/instance_document_controller_test.exs b/test/web/admin_api/controllers/instance_document_controller_test.exs
new file mode 100644
index 000000000..60dcc9dff
--- /dev/null
+++ b/test/web/admin_api/controllers/instance_document_controller_test.exs
@@ -0,0 +1,112 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.Config
+
+ @dir "test/tmp/instance_static"
+ @default_instance_panel ~s(Welcome to Pleroma!
)
+
+ setup do
+ File.mkdir_p!(@dir)
+ on_exit(fn -> File.rm_rf(@dir) end)
+ end
+
+ setup do: clear_config([:instance, :static_dir], @dir)
+
+ setup do
+ admin = insert(:user, is_admin: true)
+ token = insert(:oauth_admin_token, user: admin)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, token)
+
+ {:ok, %{admin: admin, token: token, conn: conn}}
+ end
+
+ describe "GET /api/pleroma/admin/instance_document/:name" do
+ test "return the instance document url", %{conn: conn} do
+ conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel")
+
+ assert %{"url" => url} = json_response_and_validate_schema(conn, 200)
+ index = get(build_conn(), url)
+ response = html_response(index, 200)
+ assert String.contains?(response, @default_instance_panel)
+ end
+
+ test "it returns 403 if requested by a non-admin" do
+ non_admin_user = insert(:user)
+ token = insert(:oauth_token, user: non_admin_user)
+
+ conn =
+ build_conn()
+ |> assign(:user, non_admin_user)
+ |> assign(:token, token)
+ |> get("/api/pleroma/admin/instance_document/instance-panel")
+
+ assert json_response(conn, :forbidden)
+ end
+
+ test "it returns 404 if the instance document with the given name doesn't exist", %{
+ conn: conn
+ } do
+ conn = get(conn, "/api/pleroma/admin/instance_document/1234")
+
+ assert json_response_and_validate_schema(conn, 404)
+ end
+ end
+
+ describe "PATCH /api/pleroma/admin/instance_document/:name" do
+ test "uploads the instance document", %{conn: conn} do
+ image = %Plug.Upload{
+ content_type: "text/html",
+ path: Path.absname("test/fixtures/custom_instance_panel.html"),
+ filename: "custom_instance_panel.html"
+ }
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> patch("/api/pleroma/admin/instance_document/instance-panel", %{
+ "file" => image
+ })
+
+ assert %{"url" => url} = json_response_and_validate_schema(conn, 200)
+ index = get(build_conn(), url)
+ assert html_response(index, 200) == "Custom instance panel
"
+ end
+ end
+
+ describe "DELETE /api/pleroma/admin/instance_document/:name" do
+ test "deletes the instance document", %{conn: conn} do
+ File.mkdir!(@dir <> "/instance/")
+ File.write!(@dir <> "/instance/panel.html", "Custom instance panel")
+
+ conn_resp =
+ conn
+ |> get("/api/pleroma/admin/instance_document/instance-panel")
+
+ assert %{"url" => url} = json_response_and_validate_schema(conn_resp, 200)
+ index = get(build_conn(), url)
+ assert html_response(index, 200) == "Custom instance panel"
+
+ conn
+ |> delete("/api/pleroma/admin/instance_document/instance-panel")
+ |> json_response_and_validate_schema(200)
+
+ conn_resp =
+ conn
+ |> get("/api/pleroma/admin/instance_document/instance-panel")
+
+ assert %{"url" => url} = json_response_and_validate_schema(conn_resp, 200)
+ index = get(build_conn(), url)
+ response = html_response(index, 200)
+ assert String.contains?(response, @default_instance_panel)
+ end
+ end
+end