Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F84166627
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index fdecbb700..87ae0e092 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -1,423 +1,428 @@
defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
use Pleroma.Web, :controller
require Logger
def reload(conn, _params) do
Pleroma.Emoji.reload()
conn |> text("ok")
end
@emoji_dir_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
@cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
def list_packs(conn, _params) do
pack_infos =
case File.ls(@emoji_dir_path) do
{:error, _} ->
%{}
{:ok, results} ->
results
|> Enum.filter(fn file ->
dir_path = Path.join(@emoji_dir_path, file)
# Filter to only use the pack.json packs
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end)
|> Enum.map(fn pack_name ->
pack_path = Path.join(@emoji_dir_path, pack_name)
pack_file = Path.join(pack_path, "pack.json")
{pack_name, Jason.decode!(File.read!(pack_file))}
end)
# Transform into a map of pack-name => pack-data
# Check if all the files are in place and can be sent
|> Enum.map(fn {name, pack} ->
pack_path = Path.join(@emoji_dir_path, name)
if can_download?(pack, pack_path) do
archive_for_sha = make_archive(name, pack, pack_path)
archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
{name,
pack
|> put_in(["pack", "can-download"], true)
|> put_in(["pack", "download-sha256"], archive_sha)}
else
{name,
pack
|> put_in(["pack", "can-download"], false)}
end
end)
|> Enum.into(%{})
end
conn |> json(pack_infos)
end
defp can_download?(pack, pack_path) do
# If the pack is set as shared, check if it can be downloaded
# That means that when asked, the pack can be packed and sent to the remote
# Otherwise, they'd have to download it from external-src
pack["pack"]["share-files"] and
Enum.all?(pack["files"], fn {_, path} ->
File.exists?(Path.join(pack_path, path))
end)
end
defp create_archive_and_cache(name, pack, pack_dir, md5) do
files =
['pack.json'] ++
(pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
cache_ms = :timer.seconds(@cache_seconds_per_file * Enum.count(files))
Cachex.put!(
:emoji_packs_cache,
name,
# if pack.json MD5 changes, the cache is not valid anymore
%{pack_json_md5: md5, pack_data: zip_result},
# Add a minute to cache time for every file in the pack
ttl: cache_ms
)
Logger.debug("Create an archive for the '#{name}' emoji pack, \
keeping it in cache for #{div(cache_ms, 1000)}s")
zip_result
end
defp make_archive(name, pack, pack_dir) do
# Having a different pack.json md5 invalidates cache
pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
maybe_cached_pack = Cachex.get!(:emoji_packs_cache, name)
zip_result =
if is_nil(maybe_cached_pack) do
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
else
if maybe_cached_pack[:pack_file_md5] == pack_file_md5 do
Logger.debug("Using cache for the '#{name}' shared emoji pack")
maybe_cached_pack[:pack_data]
else
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
end
end
zip_result
end
def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
pack = Jason.decode!(File.read!(pack_file))
if can_download?(pack, pack_dir) do
zip_result = make_archive(name, pack, pack_dir)
conn
|> send_download({:binary, zip_result}, filename: "#{name}.zip")
else
{:error,
conn
|> put_status(:forbidden)
|> text("Pack #{name} cannot be downloaded from this instance, either pack sharing\
was disabled for this pack or some files are missing")}
end
else
{:error,
conn
|> put_status(:not_found)
|> text("Pack #{name} does not exist")}
end
end
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
list_uri = "#{address}/api/pleroma/emoji/packs/list"
list = Tesla.get!(list_uri).body |> Jason.decode!()
full_pack = list[name]
pfiles = full_pack["files"]
pack = full_pack["pack"]
pack_info_res =
cond do
pack["share-files"] && pack["can-download"] ->
{:ok,
%{
sha: pack["download-sha256"],
uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
}}
pack["fallback-src"] ->
{:ok,
%{
sha: pack["fallback-src-sha256"],
uri: pack["fallback-src"],
fallback: true
}}
true ->
{:error, "The pack was not set as shared and there is no fallback src to download from"}
end
case pack_info_res do
{:ok, %{sha: sha, uri: uri} = pinfo} ->
sha = Base.decode16!(sha)
emoji_archive = Tesla.get!(uri).body
got_sha = :crypto.hash(:sha256, emoji_archive)
if got_sha == sha do
local_name = data["as"] || name
pack_dir = Path.join(@emoji_dir_path, local_name)
File.mkdir_p!(pack_dir)
# Fallback cannot contain a pack.json file
files =
unless(pinfo[:fallback], do: ['pack.json'], else: []) ++
(pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
# in it to depend on itself
if pinfo[:fallback] do
pack_file_path = Path.join(pack_dir, "pack.json")
File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
end
conn |> text("ok")
else
conn
|> put_status(:internal_server_error)
|> text("SHA256 for the pack doesn't match the one sent by the server")
end
{:error, e} ->
conn |> put_status(:internal_server_error) |> text(e)
end
end
def delete(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
case File.rm_rf(pack_dir) do
{:ok, _} ->
conn |> text("ok")
{:error, _} ->
conn |> put_status(:internal_server_error) |> text("Couldn't delete the pack #{name}")
end
end
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_dir = Path.join(@emoji_dir_path, name)
pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p))
# The new fallback-src is in the new data and it's not the same as it was in the old data
should_update_fb_sha =
not is_nil(new_data["fallback-src"]) and
new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
new_data =
if should_update_fb_sha do
pack_arch = Tesla.get!(new_data["fallback-src"]).body
{:ok, flist} = :zip.unzip(pack_arch, [:memory])
# Check if all files from the pack.json are in the archive
has_all_files =
Enum.all?(full_pack["files"], fn {_, from_manifest} ->
Enum.find(flist, fn {from_archive, _} ->
to_string(from_archive) == from_manifest
end)
end)
unless has_all_files do
{:error,
conn
|> put_status(:bad_request)
|> text("The fallback archive does not have all files specified in pack.json")}
else
fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
{:ok, new_data |> Map.put("fallback-src-sha256", fallback_sha)}
end
else
{:ok, new_data}
end
case new_data do
{:ok, new_data} ->
full_pack = Map.put(full_pack, "pack", new_data)
File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
# Send new data back with fallback sha filled
conn |> json(new_data)
{:error, e} ->
e
end
end
def update_file(
conn,
%{"pack_name" => pack_name, "action" => action, "shortcode" => shortcode} = params
) do
pack_dir = Path.join(@emoji_dir_path, pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p))
res =
case action do
"add" ->
unless Map.has_key?(full_pack["files"], shortcode) do
- with %{"file" => %Plug.Upload{filename: filename, path: upload_path}} <- params do
- # If there was a file name provided with the request, use it, otherwise just use the
- # uploaded file name
- filename =
- if Map.has_key?(params, "filename") do
- params["filename"]
- else
- filename
+ filename =
+ if Map.has_key?(params, "filename") do
+ params["filename"]
+ else
+ case params["file"] do
+ %Plug.Upload{filename: filename} -> filename
+ url when is_binary(url) -> Path.basename(url)
end
+ end
- unless String.trim(shortcode) |> String.length() == 0 or
- String.trim(filename) |> String.length() == 0 do
- file_path = Path.join(pack_dir, filename)
+ unless String.trim(shortcode) |> String.length() == 0 or
+ String.trim(filename) |> String.length() == 0 do
+ file_path = Path.join(pack_dir, filename)
- # If the name contains directories, create them
- if String.contains?(file_path, "/") do
- File.mkdir_p!(Path.dirname(file_path))
- end
-
- # Copy the uploaded file from the temporary directory
- File.copy!(upload_path, file_path)
+ # If the name contains directories, create them
+ if String.contains?(file_path, "/") do
+ File.mkdir_p!(Path.dirname(file_path))
+ end
- updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
+ case params["file"] do
+ %Plug.Upload{path: upload_path} ->
+ # Copy the uploaded file from the temporary directory
+ File.copy!(upload_path, file_path)
- {:ok, updated_full_pack}
- else
- {:error,
- conn
- |> put_status(:bad_request)
- |> text("shortcode or filename cannot be empty")}
+ url when is_binary(url) ->
+ # Download and write the file
+ file_contents = Tesla.get!(url).body
+ File.write!(file_path, file_contents)
end
+
+ updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
+
+ {:ok, updated_full_pack}
else
- _ -> {:error, conn |> put_status(:bad_request) |> text("\"file\" not provided")}
+ {:error,
+ conn
+ |> put_status(:bad_request)
+ |> text("shortcode or filename cannot be empty")}
end
else
{:error,
conn
|> put_status(:conflict)
|> text("An emoji with the \"#{shortcode}\" shortcode already exists")}
end
"remove" ->
if Map.has_key?(full_pack["files"], shortcode) do
{emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
emoji_file_path = Path.join(pack_dir, emoji_file_path)
# Delete the emoji file
File.rm!(emoji_file_path)
# If the old directory has no more files, remove it
if String.contains?(emoji_file_path, "/") do
dir = Path.dirname(emoji_file_path)
if Enum.empty?(File.ls!(dir)) do
File.rmdir!(dir)
end
end
{:ok, updated_full_pack}
else
{:error,
conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
end
"update" ->
if Map.has_key?(full_pack["files"], shortcode) do
with %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params do
unless String.trim(new_shortcode) |> String.length() == 0 or
String.trim(new_filename) |> String.length() == 0 do
# First, remove the old shortcode, saving the old path
{old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
new_emoji_file_path = Path.join(pack_dir, new_filename)
# If the name contains directories, create them
if String.contains?(new_emoji_file_path, "/") do
File.mkdir_p!(Path.dirname(new_emoji_file_path))
end
# Move/Rename the old filename to a new filename
# These are probably on the same filesystem, so just rename should work
:ok = File.rename(old_emoji_file_path, new_emoji_file_path)
# If the old directory has no more files, remove it
if String.contains?(old_emoji_file_path, "/") do
dir = Path.dirname(old_emoji_file_path)
if Enum.empty?(File.ls!(dir)) do
File.rmdir!(dir)
end
end
# Then, put in the new shortcode with the new path
updated_full_pack =
put_in(updated_full_pack, ["files", new_shortcode], new_filename)
{:ok, updated_full_pack}
else
{:error,
conn
|> put_status(:bad_request)
|> text("new_shortcode or new_filename cannot be empty")}
end
else
_ ->
{:error,
conn
|> put_status(:bad_request)
|> text("new_shortcode or new_file were not specified")}
end
else
{:error,
conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
end
_ ->
{:error, conn |> put_status(:bad_request) |> text("Unknown action: #{action}")}
end
case res do
{:ok, updated_full_pack} ->
# Write the emoji pack file
File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
# Return the modified file list
conn |> json(updated_full_pack["files"])
{:error, e} ->
e
end
end
end
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 6d3603da5..c1aece691 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -1,300 +1,334 @@
defmodule Pleroma.Web.EmojiAPI.EmojiAPIControllerTest do
use Pleroma.Web.ConnCase
import Tesla.Mock
import Pleroma.Factory
@emoji_dir_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
test "shared & non-shared pack information in list_packs is ok" do
conn = build_conn()
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
assert Map.has_key?(resp, "test_pack")
pack = resp["test_pack"]
assert Map.has_key?(pack["pack"], "download-sha256")
assert pack["pack"]["can-download"]
assert pack["files"] == %{"blank" => "blank.png"}
# Non-shared pack
assert Map.has_key?(resp, "test_pack_nonshared")
pack = resp["test_pack_nonshared"]
refute pack["pack"]["shared"]
refute pack["pack"]["can-download"]
end
test "downloading a shared pack from download_shared" do
conn = build_conn()
resp =
conn
|> get(emoji_api_path(conn, :download_shared, "test_pack"))
|> response(200)
{:ok, arch} = :zip.unzip(resp, [:memory])
assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end
test "downloading shared & unshared packs from another instance via download_from, deleting them" do
on_exit(fn ->
File.rm_rf!("#{@emoji_dir_path}/test_pack2")
File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
end)
mock(fn
%{
method: :get,
url: "https://example.com/api/pleroma/emoji/packs/list"
} ->
conn = build_conn()
conn
|> get(emoji_api_path(conn, :list_packs))
|> json_response(200)
|> json()
%{
method: :get,
url: "https://example.com/api/pleroma/emoji/packs/download_shared/test_pack"
} ->
conn = build_conn()
conn
|> get(emoji_api_path(conn, :download_shared, "test_pack"))
|> response(200)
|> text()
%{
method: :get,
url: "https://nonshared-pack"
} ->
text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
end)
admin = insert(:user, info: %{is_admin: true})
conn = build_conn() |> assign(:user, admin)
assert conn
|> put_req_header("content-type", "application/json")
|> post(
emoji_api_path(
conn,
:download_from
),
%{
instance_address: "https://example.com",
pack_name: "test_pack",
as: "test_pack2"
}
|> Jason.encode!()
)
|> text_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_pack2/pack.json")
assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
assert conn
|> delete(emoji_api_path(conn, :delete, "test_pack2"))
|> response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_pack2")
# non-shared, downloaded from the fallback URL
conn = build_conn() |> assign(:user, admin)
assert conn
|> put_req_header("content-type", "application/json")
|> post(
emoji_api_path(
conn,
:download_from
),
%{
instance_address: "https://example.com",
pack_name: "test_pack_nonshared",
as: "test_pack_nonshared2"
}
|> Jason.encode!()
)
|> text_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/pack.json")
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
assert conn
|> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
|> response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_pack_nonshared2")
end
describe "updating pack metadata" do
setup do
pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
end)
{:ok,
admin: insert(:user, info: %{is_admin: true}),
pack_file: pack_file,
new_data: %{
"license" => "Test license changed",
"homepage" => "https://pleroma.social",
"description" => "Test description",
"share-files" => false
}}
end
test "for a pack without a fallback source", ctx do
conn = build_conn()
assert conn
|> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
"new_data" => ctx[:new_data]
}
)
|> json_response(200) == ctx[:new_data]
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data]
end
test "for a pack with a fallback source", ctx do
mock(fn
%{
method: :get,
url: "https://nonshared-pack"
} ->
text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
new_data_with_sha =
Map.put(
new_data,
"fallback-src-sha256",
"74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF"
)
conn = build_conn()
assert conn
|> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
"new_data" => new_data
}
)
|> json_response(200) == new_data_with_sha
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
end
test "when the fallback source doesn't have all the files", ctx do
mock(fn
%{
method: :get,
url: "https://nonshared-pack"
} ->
{:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory])
text(empty_arch)
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
conn = build_conn()
assert conn
|> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
"new_data" => new_data
}
)
|> text_response(:bad_request) =~ "does not have all"
end
end
test "updating pack files" do
pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/blank_url.png")
File.rm_rf!("#{@emoji_dir_path}/test_pack/dir")
File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2")
end)
admin = insert(:user, info: %{is_admin: true})
conn = build_conn()
same_name = %{
"action" => "add",
"shortcode" => "blank",
"filename" => "dir/blank.png",
"file" => %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_dir_path}/test_pack/blank.png"
}
}
different_name = %{same_name | "shortcode" => "blank_2"}
conn = conn |> assign(:user, admin)
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
|> text_response(:conflict) =~ "already exists"
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), different_name)
|> json_response(200) == %{"blank" => "blank.png", "blank_2" => "dir/blank.png"}
assert File.exists?("#{@emoji_dir_path}/test_pack/dir/blank.png")
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), %{
"action" => "update",
"shortcode" => "blank_2",
"new_shortcode" => "blank_3",
"new_filename" => "dir_2/blank_3.png"
})
|> json_response(200) == %{"blank" => "blank.png", "blank_3" => "dir_2/blank_3.png"}
refute File.exists?("#{@emoji_dir_path}/test_pack/dir/")
assert File.exists?("#{@emoji_dir_path}/test_pack/dir_2/blank_3.png")
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), %{
"action" => "remove",
"shortcode" => "blank_3"
})
|> json_response(200) == %{"blank" => "blank.png"}
refute File.exists?("#{@emoji_dir_path}/test_pack/dir_2/")
+
+ mock(fn
+ %{
+ method: :get,
+ url: "https://test-blank/blank_url.png"
+ } ->
+ text(File.read!("#{@emoji_dir_path}/test_pack/blank.png"))
+ end)
+
+ # The name should be inferred from the URL ending
+ from_url = %{
+ "action" => "add",
+ "shortcode" => "blank_url",
+ "file" => "https://test-blank/blank_url.png"
+ }
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), from_url)
+ |> json_response(200) == %{
+ "blank" => "blank.png",
+ "blank_url" => "blank_url.png"
+ }
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "remove",
+ "shortcode" => "blank_url"
+ })
+ |> json_response(200) == %{"blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jun 4, 7:09 PM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1539280
Default Alt Text
(25 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment