mirror of
https://akkoma.dev/AkkomaGang/akkoma.git
synced 2024-12-01 17:38:53 +00:00
ddd79ff22d
No new path traversal attacks are known. But given the many entrypoints and code flow complexity inside pack.ex, it unfortunately seems possible a future refactor or addition might reintroduce one. Furthermore, some old packs might still contain traversing path entries which could trigger undesireable actions on rename or delete. To ensure this can never happen, assert safety during path construction. Path.safe_relative was introduced in Elixir 1.14, but fortunately, we already require at least 1.14 anyway.
706 lines
20 KiB
Elixir
706 lines
20 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Emoji.Pack do
|
|
@derive {Jason.Encoder, only: [:files, :pack, :files_count]}
|
|
defstruct files: %{},
|
|
files_count: 0,
|
|
pack_file: nil,
|
|
path: nil,
|
|
pack: %{},
|
|
name: nil
|
|
|
|
@type t() :: %__MODULE__{
|
|
files: %{String.t() => Path.t()},
|
|
files_count: non_neg_integer(),
|
|
pack_file: Path.t(),
|
|
path: Path.t(),
|
|
pack: map(),
|
|
name: String.t()
|
|
}
|
|
|
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
|
|
|
alias Pleroma.Emoji
|
|
alias Pleroma.Emoji.Pack
|
|
alias Pleroma.Utils
|
|
|
|
# Invalid/Malicious names are supposed to be filtered out before path joining,
|
|
# but there are many entrypoints to affected functions so as the code changes
|
|
# we might accidentally let an unsanitised name slip through.
|
|
# To make sure, use the below which crash the process otherwise.
|
|
|
|
# ALWAYS use this when constructing paths from external name!
|
|
# (name meaning it must be only a single path component)
|
|
defp path_join_name_safe(dir, name) do
|
|
if to_string(name) != Path.basename(name) or name in ["..", ".", ""] do
|
|
raise "Invalid or malicious pack name: #{name}"
|
|
else
|
|
Path.join(dir, name)
|
|
end
|
|
end
|
|
|
|
# ALWAYS use this to join external paths
|
|
# (which are allowed to have several components)
|
|
defp path_join_safe(dir, path) do
|
|
{:ok, safe_path} = Path.safe_relative(path)
|
|
Path.join(dir, safe_path)
|
|
end
|
|
|
|
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
|
def create(name) do
|
|
with :ok <- validate_not_empty([name]),
|
|
dir <- path_join_name_safe(emoji_path(), name),
|
|
:ok <- File.mkdir(dir) do
|
|
save_pack(%__MODULE__{
|
|
path: dir,
|
|
pack_file: Path.join(dir, "pack.json")
|
|
})
|
|
end
|
|
end
|
|
|
|
defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size)
|
|
|
|
defp paginate(entities, page, page_size) do
|
|
entities
|
|
|> Enum.chunk_every(page_size)
|
|
|> Enum.at(page - 1)
|
|
end
|
|
|
|
@spec show(keyword()) :: {:ok, t()} | {:error, atom()}
|
|
def show(opts) do
|
|
name = opts[:name]
|
|
|
|
with :ok <- validate_not_empty([name]),
|
|
{:ok, pack} <- load_pack(name) do
|
|
shortcodes =
|
|
pack.files
|
|
|> Map.keys()
|
|
|> Enum.sort()
|
|
|> paginate(opts[:page], opts[:page_size])
|
|
|
|
pack = Map.put(pack, :files, Map.take(pack.files, shortcodes))
|
|
|
|
{:ok, validate_pack(pack)}
|
|
end
|
|
end
|
|
|
|
@spec delete(String.t()) ::
|
|
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
|
def delete(name) do
|
|
with :ok <- validate_not_empty([name]),
|
|
pack_path <- path_join_name_safe(emoji_path(), name) do
|
|
File.rm_rf(pack_path)
|
|
end
|
|
end
|
|
|
|
@spec unpack_zip_emojies(list(tuple())) :: list(map())
|
|
defp unpack_zip_emojies(zip_files) do
|
|
Enum.reduce(zip_files, [], fn
|
|
{_, path, s, _, _, _}, acc when elem(s, 2) == :regular ->
|
|
with(
|
|
filename <- Path.basename(path),
|
|
shortcode <- Path.basename(filename, Path.extname(filename)),
|
|
false <- Emoji.exist?(shortcode)
|
|
) do
|
|
[%{path: path, filename: path, shortcode: shortcode} | acc]
|
|
else
|
|
_ -> acc
|
|
end
|
|
|
|
_, acc ->
|
|
acc
|
|
end)
|
|
end
|
|
|
|
@spec add_file(t(), String.t(), Path.t(), Plug.Upload.t() | binary()) ::
|
|
{:ok, t()}
|
|
| {:error, File.posix() | atom()}
|
|
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
|
|
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
|
|
[_ | _] = emojies <- unpack_zip_emojies(zip_files),
|
|
{:ok, tmp_dir} <- Utils.tmp_dir("emoji") do
|
|
try do
|
|
{:ok, _emoji_files} =
|
|
:zip.unzip(
|
|
to_charlist(file.path),
|
|
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
|
|
)
|
|
|
|
{_, updated_pack} =
|
|
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
|
|
emoji_file = %Plug.Upload{
|
|
filename: item[:filename],
|
|
path: path_join_safe(tmp_dir, item[:path])
|
|
}
|
|
|
|
{:ok, updated_pack} =
|
|
do_add_file(
|
|
emoji_pack,
|
|
item[:shortcode],
|
|
to_string(item[:filename]),
|
|
emoji_file
|
|
)
|
|
|
|
{item, updated_pack}
|
|
end)
|
|
|
|
Emoji.reload()
|
|
|
|
{:ok, updated_pack}
|
|
after
|
|
File.rm_rf(tmp_dir)
|
|
end
|
|
else
|
|
{:error, _} = error ->
|
|
error
|
|
|
|
_ ->
|
|
{:ok, pack}
|
|
end
|
|
end
|
|
|
|
def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do
|
|
try_add_file(pack, shortcode, filename, file)
|
|
end
|
|
|
|
def add_file(%Pack{} = pack, shortcode, filename, filedata) when is_binary(filedata) do
|
|
try_add_file(pack, shortcode, filename, filedata)
|
|
end
|
|
|
|
defp try_add_file(%Pack{} = pack, shortcode, filename, file) do
|
|
with :ok <- validate_not_empty([shortcode, filename]),
|
|
:ok <- validate_emoji_not_exists(shortcode),
|
|
{:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do
|
|
Emoji.reload()
|
|
{:ok, updated_pack}
|
|
end
|
|
end
|
|
|
|
defp do_add_file(pack, shortcode, filename, file) do
|
|
with :ok <- save_file(file, pack, filename) do
|
|
pack
|
|
|> put_emoji(shortcode, filename)
|
|
|> save_pack()
|
|
end
|
|
end
|
|
|
|
@spec delete_file(t(), String.t()) ::
|
|
{:ok, t()} | {:error, File.posix() | atom()}
|
|
def delete_file(%Pack{} = pack, shortcode) do
|
|
with :ok <- validate_not_empty([shortcode]),
|
|
:ok <- remove_file(pack, shortcode),
|
|
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
|
|
Emoji.reload()
|
|
{:ok, updated_pack}
|
|
end
|
|
end
|
|
|
|
@spec update_file(t(), String.t(), String.t(), String.t(), boolean()) ::
|
|
{:ok, t()} | {:error, File.posix() | atom()}
|
|
def update_file(%Pack{} = pack, shortcode, new_shortcode, new_filename, force) do
|
|
with :ok <- validate_not_empty([shortcode, new_shortcode, new_filename]),
|
|
{:ok, filename} <- get_filename(pack, shortcode),
|
|
:ok <- validate_emoji_not_exists(new_shortcode, force),
|
|
:ok <- rename_file(pack, filename, new_filename),
|
|
{:ok, updated_pack} <-
|
|
pack
|
|
|> delete_emoji(shortcode)
|
|
|> put_emoji(new_shortcode, new_filename)
|
|
|> save_pack() do
|
|
Emoji.reload()
|
|
{:ok, updated_pack}
|
|
end
|
|
end
|
|
|
|
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
|
|
def import_from_filesystem do
|
|
emoji_path = emoji_path()
|
|
|
|
with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
|
|
{:ok, results} <- File.ls(emoji_path) do
|
|
names =
|
|
results
|
|
# items come from File.ls, thus safe
|
|
|> Enum.map(&Path.join(emoji_path, &1))
|
|
|> Enum.reject(fn path ->
|
|
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
|
end)
|
|
|> Enum.map(&write_pack_contents/1)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
{:ok, names}
|
|
else
|
|
{:ok, %{access: _}} -> {:error, :no_read_write}
|
|
e -> e
|
|
end
|
|
end
|
|
|
|
@spec list_remote(keyword()) :: {:ok, map()} | {:error, atom()}
|
|
def list_remote(opts) do
|
|
uri = opts[:url] |> String.trim() |> URI.parse()
|
|
|
|
with :ok <- validate_shareable_packs_available(uri) do
|
|
uri
|
|
|> URI.merge(
|
|
"/api/v1/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}"
|
|
)
|
|
|> http_get()
|
|
end
|
|
end
|
|
|
|
@spec list_local(keyword()) :: {:ok, map(), non_neg_integer()}
|
|
def list_local(opts) do
|
|
with {:ok, results} <- list_packs_dir() do
|
|
all_packs =
|
|
results
|
|
|> Enum.map(fn name ->
|
|
case load_pack(name) do
|
|
{:ok, pack} -> pack
|
|
_ -> nil
|
|
end
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
packs =
|
|
all_packs
|
|
|> paginate(opts[:page], opts[:page_size])
|
|
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
|
|
|
|
{:ok, packs, length(all_packs)}
|
|
end
|
|
end
|
|
|
|
@spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()}
|
|
def get_archive(name) do
|
|
with {:ok, pack} <- load_pack(name),
|
|
:ok <- validate_downloadable(pack) do
|
|
{:ok, fetch_archive(pack)}
|
|
end
|
|
end
|
|
|
|
@spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()}
|
|
def download(name, url, as) do
|
|
uri = url |> String.trim() |> URI.parse()
|
|
|
|
with :ok <- validate_shareable_packs_available(uri),
|
|
{:ok, remote_pack} <-
|
|
uri |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{URI.encode(name)}") |> http_get(),
|
|
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
|
|
{:ok, archive} <- download_archive(url, sha),
|
|
pack <- copy_as(remote_pack, as || name),
|
|
{:ok, _} = unzip(archive, pack_info, remote_pack, pack) do
|
|
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
|
|
# in it to depend on itself
|
|
if pack_info[:fallback] do
|
|
save_pack(pack)
|
|
else
|
|
{:ok, pack}
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
|
|
def save_metadata(metadata, %__MODULE__{} = pack) do
|
|
pack
|
|
|> Map.put(:pack, metadata)
|
|
|> save_pack()
|
|
end
|
|
|
|
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
|
|
def update_metadata(name, data) do
|
|
with {:ok, pack} <- load_pack(name) do
|
|
if fallback_sha_changed?(pack, data) do
|
|
update_sha_and_save_metadata(pack, data)
|
|
else
|
|
save_metadata(data, pack)
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
|
|
def load_pack(name) do
|
|
pack_dir = path_join_name_safe(emoji_path(), name)
|
|
pack_file = Path.join(pack_dir, "pack.json")
|
|
|
|
with {:ok, _} <- File.stat(pack_file),
|
|
{:ok, pack_data} <- File.read(pack_file) do
|
|
pack =
|
|
from_json(
|
|
pack_data,
|
|
%{
|
|
pack_file: pack_file,
|
|
path: Path.dirname(pack_file),
|
|
name: name
|
|
}
|
|
)
|
|
|
|
files_count =
|
|
pack.files
|
|
|> Map.keys()
|
|
|> length()
|
|
|
|
{:ok, Map.put(pack, :files_count, files_count)}
|
|
end
|
|
end
|
|
|
|
@spec emoji_path() :: Path.t()
|
|
defp emoji_path do
|
|
[:instance, :static_dir]
|
|
|> Pleroma.Config.get!()
|
|
|> Path.join("emoji")
|
|
end
|
|
|
|
defp validate_emoji_not_exists(shortcode, force \\ false)
|
|
defp validate_emoji_not_exists(_shortcode, true), do: :ok
|
|
|
|
defp validate_emoji_not_exists(shortcode, _) do
|
|
if Emoji.exist?(shortcode) do
|
|
{:error, :already_exists}
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp write_pack_contents(path) do
|
|
pack = %__MODULE__{
|
|
files: files_from_path(path),
|
|
path: path,
|
|
pack_file: Path.join(path, "pack.json")
|
|
}
|
|
|
|
case save_pack(pack) do
|
|
{:ok, _pack} -> Path.basename(path)
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp files_from_path(path) do
|
|
txt_path = Path.join(path, "emoji.txt")
|
|
|
|
if File.exists?(txt_path) do
|
|
# There's an emoji.txt file, it's likely from a pack installed by the pack manager.
|
|
# Make a pack.json file from the contents of that emoji.txt file
|
|
|
|
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
|
|
|
|
# Create a map of shortcodes to filenames from emoji.txt
|
|
txt_path
|
|
|> File.read!()
|
|
|> String.split("\n")
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.map(fn line ->
|
|
case String.split(line, ~r/,\s*/) do
|
|
# This matches both strings with and without tags
|
|
# and we don't care about tags here
|
|
[name, file | _] ->
|
|
file_dir_name = Path.dirname(file)
|
|
|
|
if String.ends_with?(path, file_dir_name) do
|
|
{name, Path.basename(file)}
|
|
else
|
|
{name, file}
|
|
end
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Map.new()
|
|
else
|
|
# If there's no emoji.txt, assume all files
|
|
# that are of certain extensions from the config are emojis and import them all
|
|
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
|
|
Emoji.Loader.make_shortcode_to_file_map(path, pack_extensions)
|
|
end
|
|
end
|
|
|
|
defp validate_pack(pack) do
|
|
info =
|
|
if downloadable?(pack) do
|
|
archive = fetch_archive(pack)
|
|
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
|
|
|
|
pack.pack
|
|
|> Map.put("can-download", true)
|
|
|> Map.put("download-sha256", archive_sha)
|
|
else
|
|
Map.put(pack.pack, "can-download", false)
|
|
end
|
|
|
|
Map.put(pack, :pack, info)
|
|
end
|
|
|
|
defp downloadable?(pack) 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"] &&
|
|
Enum.all?(pack.files, fn {_, file} ->
|
|
pack.path
|
|
|> Path.join(file)
|
|
|> File.exists?()
|
|
end)
|
|
end
|
|
|
|
defp create_archive_and_cache(pack, hash) do
|
|
files = [
|
|
~c"pack.json"
|
|
| Enum.map(pack.files, fn {_, file} ->
|
|
{:ok, file} = Path.safe_relative(file)
|
|
to_charlist(file)
|
|
end)
|
|
]
|
|
|
|
{:ok, {_, result}} =
|
|
:zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)])
|
|
|
|
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
|
|
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
|
|
|
|
@cachex.put(
|
|
:emoji_packs_cache,
|
|
pack.name,
|
|
# if pack.json MD5 changes, the cache is not valid anymore
|
|
%{hash: hash, pack_data: result},
|
|
# Add a minute to cache time for every file in the pack
|
|
ttl: overall_ttl
|
|
)
|
|
|
|
result
|
|
end
|
|
|
|
defp save_pack(pack) do
|
|
with {:ok, json} <- Jason.encode(pack, pretty: true),
|
|
:ok <- File.write(pack.pack_file, json) do
|
|
{:ok, pack}
|
|
end
|
|
end
|
|
|
|
defp from_json(json, attrs) do
|
|
map = Jason.decode!(json)
|
|
|
|
pack_attrs =
|
|
attrs
|
|
|> Map.merge(%{
|
|
files: map["files"],
|
|
pack: map["pack"]
|
|
})
|
|
|
|
struct(__MODULE__, pack_attrs)
|
|
end
|
|
|
|
defp validate_shareable_packs_available(uri) do
|
|
with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
|
|
# Get the actual nodeinfo address and fetch it
|
|
{:ok, %{"metadata" => %{"features" => features}}} <-
|
|
links |> List.last() |> Map.get("href") |> http_get() do
|
|
if Enum.member?(features, "shareable_emoji_packs") do
|
|
:ok
|
|
else
|
|
{:error, :not_shareable}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp validate_not_empty(list) do
|
|
if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
|
|
:ok
|
|
else
|
|
{:error, :empty_values}
|
|
end
|
|
end
|
|
|
|
defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do
|
|
file_path = path_join_safe(pack.path, filename)
|
|
create_subdirs(file_path)
|
|
|
|
with {:ok, _} <- File.copy(upload_path, file_path) do
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp save_file(file_data, pack, filename) when is_binary(file_data) do
|
|
file_path = path_join_safe(pack.path, filename)
|
|
create_subdirs(file_path)
|
|
File.write(file_path, file_data, [:binary])
|
|
end
|
|
|
|
defp put_emoji(pack, shortcode, filename) do
|
|
files = Map.put(pack.files, shortcode, filename)
|
|
%{pack | files: files, files_count: length(Map.keys(files))}
|
|
end
|
|
|
|
defp delete_emoji(pack, shortcode) do
|
|
files = Map.delete(pack.files, shortcode)
|
|
%{pack | files: files}
|
|
end
|
|
|
|
defp rename_file(pack, filename, new_filename) do
|
|
old_path = path_join_safe(pack.path, filename)
|
|
new_path = path_join_safe(pack.path, new_filename)
|
|
create_subdirs(new_path)
|
|
|
|
with :ok <- File.rename(old_path, new_path) do
|
|
remove_dir_if_empty(old_path, filename)
|
|
end
|
|
end
|
|
|
|
defp create_subdirs(file_path) do
|
|
with true <- String.contains?(file_path, "/"),
|
|
path <- Path.dirname(file_path),
|
|
false <- File.exists?(path) do
|
|
File.mkdir_p!(path)
|
|
end
|
|
end
|
|
|
|
defp remove_file(pack, shortcode) do
|
|
with {:ok, filename} <- get_filename(pack, shortcode),
|
|
emoji <- path_join_safe(pack.path, filename),
|
|
:ok <- File.rm(emoji) do
|
|
remove_dir_if_empty(emoji, filename)
|
|
end
|
|
end
|
|
|
|
defp remove_dir_if_empty(emoji, filename) do
|
|
dir = Path.dirname(emoji)
|
|
|
|
if String.contains?(filename, "/") and File.ls!(dir) == [] do
|
|
File.rmdir!(dir)
|
|
else
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp get_filename(pack, shortcode) do
|
|
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
|
|
file_path <- path_join_safe(pack.path, filename),
|
|
{:ok, _} <- File.stat(file_path) do
|
|
{:ok, filename}
|
|
else
|
|
{:error, _} = error ->
|
|
error
|
|
|
|
_ ->
|
|
{:error, :doesnt_exist}
|
|
end
|
|
end
|
|
|
|
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
|
|
|
|
defp http_get(url) do
|
|
with {:ok, %{body: body}} <- Pleroma.HTTP.get(url, [], []) do
|
|
Jason.decode(body)
|
|
end
|
|
end
|
|
|
|
defp list_packs_dir do
|
|
emoji_path = emoji_path()
|
|
# Create the directory first if it does not exist. This is probably the first request made
|
|
# with the API so it should be sufficient
|
|
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
|
|
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
|
|
{:ok, Enum.sort(results)}
|
|
else
|
|
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
|
|
{:ls, {:error, e}} -> {:error, :ls, e}
|
|
end
|
|
end
|
|
|
|
defp validate_downloadable(pack) do
|
|
if downloadable?(pack), do: :ok, else: {:error, :cant_download}
|
|
end
|
|
|
|
defp copy_as(remote_pack, local_name) do
|
|
path = path_join_name_safe(emoji_path(), local_name)
|
|
|
|
%__MODULE__{
|
|
name: local_name,
|
|
path: path,
|
|
files: remote_pack["files"],
|
|
pack_file: Path.join(path, "pack.json")
|
|
}
|
|
end
|
|
|
|
defp unzip(archive, pack_info, remote_pack, local_pack) do
|
|
with :ok <- File.mkdir_p!(local_pack.path) do
|
|
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
|
|
# Fallback cannot contain a pack.json file
|
|
files = if pack_info[:fallback], do: files, else: [~c"pack.json" | files]
|
|
|
|
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
|
|
end
|
|
end
|
|
|
|
defp fetch_pack_info(remote_pack, uri, name) do
|
|
case remote_pack["pack"] do
|
|
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
|
{:ok,
|
|
%{
|
|
sha: sha,
|
|
url:
|
|
URI.merge(uri, "/api/v1/pleroma/emoji/packs/archive?name=#{URI.encode(name)}")
|
|
|> to_string()
|
|
}}
|
|
|
|
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
|
{:ok,
|
|
%{
|
|
sha: sha,
|
|
url: src,
|
|
fallback: true
|
|
}}
|
|
|
|
_ ->
|
|
{:error, "The pack was not set as shared and there is no fallback src to download from"}
|
|
end
|
|
end
|
|
|
|
defp download_archive(url, sha) do
|
|
with {:ok, %{body: archive}} <- Pleroma.HTTP.get(url) do
|
|
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
|
|
{:ok, archive}
|
|
else
|
|
{:error, :invalid_checksum}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp fetch_archive(pack) do
|
|
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
|
|
|
|
case @cachex.get!(:emoji_packs_cache, pack.name) do
|
|
%{hash: ^hash, pack_data: archive} -> archive
|
|
_ -> create_archive_and_cache(pack, hash)
|
|
end
|
|
end
|
|
|
|
defp fallback_sha_changed?(pack, data) do
|
|
is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"]
|
|
end
|
|
|
|
defp update_sha_and_save_metadata(pack, data) do
|
|
with {:ok, %{body: zip}} <- Pleroma.HTTP.get(data[:"fallback-src"]),
|
|
:ok <- validate_has_all_files(pack, zip) do
|
|
fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
|
|
|
|
data
|
|
|> Map.put("fallback-src-sha256", fallback_sha)
|
|
|> save_metadata(pack)
|
|
end
|
|
end
|
|
|
|
defp validate_has_all_files(pack, zip) do
|
|
with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
|
|
# Check if all files from the pack.json are in the archive
|
|
pack.files
|
|
|> Enum.all?(fn {_, from_manifest} ->
|
|
List.keyfind(f_list, to_charlist(from_manifest), 0)
|
|
end)
|
|
|> if(do: :ok, else: {:error, :incomplete})
|
|
end
|
|
end
|
|
end
|