Merge branch 'instance-docs' into 'develop'

AdminAPI: Allow to modify Terms of Service and Instance Panel via Admin API

Closes #1516

See merge request pleroma/pleroma!2931
This commit is contained in:
feld 2020-09-17 16:54:53 +00:00
commit 608824b97e
8 changed files with 377 additions and 0 deletions

View File

@ -42,6 +42,12 @@ switched to a new configuration mechanism, however it was not officially removed
### Added
- Rich media failure tracking (along with `:failure_backoff` option).
<details>
<summary>Admin API Changes</summary>
- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API
</details>
### 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

View File

@ -1455,3 +1455,45 @@ Loads json generated from `config/descriptions.exs`.
"unread": false
}
```
## `GET /api/pleroma/admin/instance_document/:document_name`
### Get an instance document
- Authentication: required
- Response:
Returns the content of the document
```html
<h1>Instance panel</h1>
```
## `PATCH /api/pleroma/admin/instance_document/:document_name`
- Params:
- `file` (the file to be uploaded, using multipart form data.)
### Update 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"
}
```

View File

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.InstanceStatic
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),
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
conn
|> put_resp_content_type("text/html")
|> send_resp(200, content)
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

View File

@ -0,0 +1,115 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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 => document_content(),
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
defp document_content do
Operation.response("InstanceDocumentContent", "text/html", %Schema{
type: :string,
example: "<h1>Instance panel</h1>"
})
end
end

View File

@ -0,0 +1,62 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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}
_ -> {: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

View File

@ -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)

View File

@ -0,0 +1 @@
<h2>Custom instance panel</h2>

View File

@ -0,0 +1,106 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>)
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 content = html_response(conn, 200)
assert String.contains?(content, @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) == "<h2>Custom instance panel</h2>"
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 html_response(conn_resp, 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 content = html_response(conn_resp, 200)
assert String.contains?(content, @default_instance_panel)
end
end
end