aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/upload
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/upload')
-rw-r--r--lib/pleroma/upload/filter.ex46
-rw-r--r--lib/pleroma/upload/filter/analyze_metadata.ex83
-rw-r--r--lib/pleroma/upload/filter/anonymize_filename.ex38
-rw-r--r--lib/pleroma/upload/filter/dedupe.ex24
-rw-r--r--lib/pleroma/upload/filter/exiftool/read_description.ex52
-rw-r--r--lib/pleroma/upload/filter/exiftool/strip_location.ex32
-rw-r--r--lib/pleroma/upload/filter/mogrifun.ex53
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex48
8 files changed, 376 insertions, 0 deletions
diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex
new file mode 100644
index 0000000..717f066
--- /dev/null
+++ b/lib/pleroma/upload/filter.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter do
+ @moduledoc """
+ Upload Filter behaviour
+
+ This behaviour allows to run filtering actions just before a file is uploaded. This allows to:
+
+ * morph in place the temporary file
+ * change any field of a `Pleroma.Upload` struct
+ * cancel/stop the upload
+ """
+
+ require Logger
+
+ @callback filter(upload :: struct()) ::
+ {:ok, :filtered}
+ | {:ok, :noop}
+ | {:ok, :filtered, upload :: struct()}
+ | {:error, any()}
+
+ @spec filter([module()], upload :: struct()) :: {:ok, upload :: struct()} | {:error, any()}
+
+ def filter([], upload) do
+ {:ok, upload}
+ end
+
+ def filter([filter | rest], upload) do
+ case filter.filter(upload) do
+ {:ok, :filtered} ->
+ filter(rest, upload)
+
+ {:ok, :filtered, upload} ->
+ filter(rest, upload)
+
+ {:ok, :noop} ->
+ filter(rest, upload)
+
+ error ->
+ Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
+ error
+ end
+ end
+end
diff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex
new file mode 100644
index 0000000..9a76a99
--- /dev/null
+++ b/lib/pleroma/upload/filter/analyze_metadata.ex
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
+ @moduledoc """
+ Extracts metadata about the upload, such as width/height
+ """
+ require Logger
+
+ @behaviour Pleroma.Upload.Filter
+
+ @spec filter(Pleroma.Upload.t()) ::
+ {:ok, :filtered, Pleroma.Upload.t()} | {:ok, :noop} | {:error, String.t()}
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _} = upload) do
+ try do
+ image =
+ file
+ |> Mogrify.open()
+ |> Mogrify.verbose()
+
+ upload =
+ upload
+ |> Map.put(:width, image.width)
+ |> Map.put(:height, image.height)
+ |> Map.put(:blurhash, get_blurhash(file))
+
+ {:ok, :filtered, upload}
+ rescue
+ e in ErlangError ->
+ Logger.warn("#{__MODULE__}: #{inspect(e)}")
+ {:ok, :noop}
+ end
+ end
+
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "video" <> _} = upload) do
+ try do
+ result = media_dimensions(file)
+
+ upload =
+ upload
+ |> Map.put(:width, result.width)
+ |> Map.put(:height, result.height)
+
+ {:ok, :filtered, upload}
+ rescue
+ e in ErlangError ->
+ Logger.warn("#{__MODULE__}: #{inspect(e)}")
+ {:ok, :noop}
+ end
+ end
+
+ def filter(_), do: {:ok, :noop}
+
+ defp get_blurhash(file) do
+ with {:ok, blurhash} <- :eblurhash.magick(file) do
+ blurhash
+ else
+ _ -> nil
+ end
+ end
+
+ defp media_dimensions(file) do
+ with executable when is_binary(executable) <- System.find_executable("ffprobe"),
+ args = [
+ "-v",
+ "error",
+ "-show_entries",
+ "stream=width,height",
+ "-of",
+ "csv=p=0:s=x",
+ file
+ ],
+ {result, 0} <- System.cmd(executable, args),
+ [width, height] <-
+ String.split(String.trim(result), "x") |> Enum.map(&String.to_integer(&1)) do
+ %{width: width, height: height}
+ else
+ nil -> {:error, {:ffprobe, :command_not_found}}
+ {:error, _} = error -> error
+ end
+ end
+end
diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex
new file mode 100644
index 0000000..234ccb6
--- /dev/null
+++ b/lib/pleroma/upload/filter/anonymize_filename.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.AnonymizeFilename do
+ @moduledoc """
+ Replaces the original filename with a pre-defined text or randomly generated string.
+
+ Should be used after `Pleroma.Upload.Filter.Dedupe`.
+ """
+ @behaviour Pleroma.Upload.Filter
+
+ alias Pleroma.Config
+ alias Pleroma.Upload
+
+ def filter(%Upload{name: name} = upload) do
+ extension = List.last(String.split(name, "."))
+ name = predefined_name(extension) || random(extension)
+ {:ok, :filtered, %Upload{upload | name: name}}
+ end
+
+ def filter(_), do: {:ok, :noop}
+
+ @spec predefined_name(String.t()) :: String.t() | nil
+ defp predefined_name(extension) do
+ with name when not is_nil(name) <- Config.get([__MODULE__, :text]),
+ do: String.replace(name, "{extension}", extension)
+ end
+
+ defp random(extension) do
+ string =
+ 10
+ |> :crypto.strong_rand_bytes()
+ |> Base.url_encode64(padding: false)
+
+ string <> "." <> extension
+ end
+end
diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex
new file mode 100644
index 0000000..ef793d3
--- /dev/null
+++ b/lib/pleroma/upload/filter/dedupe.ex
@@ -0,0 +1,24 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Dedupe do
+ @behaviour Pleroma.Upload.Filter
+ alias Pleroma.Upload
+
+ def filter(%Upload{name: name, tempfile: tempfile} = upload) do
+ extension =
+ name
+ |> String.split(".")
+ |> List.last()
+
+ shasum =
+ :crypto.hash(:sha256, File.read!(tempfile))
+ |> Base.encode16(case: :lower)
+
+ filename = shasum <> "." <> extension
+ {:ok, :filtered, %Upload{upload | id: shasum, path: filename}}
+ end
+
+ def filter(_), do: {:ok, :noop}
+end
diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex
new file mode 100644
index 0000000..543b220
--- /dev/null
+++ b/lib/pleroma/upload/filter/exiftool/read_description.ex
@@ -0,0 +1,52 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
+ @moduledoc """
+ Gets a valid description from the related EXIF tags and provides them in the response if no description is provided yet.
+ It will first check ImageDescription, when that doesn't probide a valid description, it will check iptc:Caption-Abstract.
+ A valid description means the fields are filled in and not too long (see `:instance, :description_limit`).
+ """
+ @behaviour Pleroma.Upload.Filter
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
+
+ def filter(%Pleroma.Upload{description: description})
+ when is_binary(description),
+ do: {:ok, :noop}
+
+ def filter(%Pleroma.Upload{tempfile: file} = upload),
+ do: {:ok, :filtered, upload |> Map.put(:description, read_description_from_exif_data(file))}
+
+ def filter(_, _), do: {:ok, :noop}
+
+ defp read_description_from_exif_data(file) do
+ nil
+ |> read_when_empty(file, "-ImageDescription")
+ |> read_when_empty(file, "-iptc:Caption-Abstract")
+ end
+
+ defp read_when_empty(current_description, _, _) when is_binary(current_description),
+ do: current_description
+
+ defp read_when_empty(_, file, tag) do
+ try do
+ {tag_content, 0} =
+ System.cmd("exiftool", ["-b", "-s3", tag, file],
+ stderr_to_stdout: false,
+ parallelism: true
+ )
+
+ tag_content = String.trim(tag_content)
+
+ if tag_content != "" and
+ String.length(tag_content) <=
+ Pleroma.Config.get([:instance, :description_limit]),
+ do: tag_content,
+ else: nil
+ rescue
+ _ in ErlangError -> nil
+ end
+ end
+end
diff --git a/lib/pleroma/upload/filter/exiftool/strip_location.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex
new file mode 100644
index 0000000..f2bcc46
--- /dev/null
+++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
+ @moduledoc """
+ Strips GPS related EXIF tags and overwrites the file in place.
+ Also strips or replaces filesystem metadata e.g., timestamps.
+ """
+ @behaviour Pleroma.Upload.Filter
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
+
+ # Formats not compatible with exiftool at this time
+ def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}
+ def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
+ def filter(%Pleroma.Upload{content_type: "image/svg" <> _}), do: {:ok, :noop}
+
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
+ try do
+ case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
+ {_response, 0} -> {:ok, :filtered}
+ {error, 1} -> {:error, error}
+ end
+ rescue
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
+ end
+ end
+
+ def filter(_), do: {:ok, :noop}
+end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
new file mode 100644
index 0000000..a0f247b
--- /dev/null
+++ b/lib/pleroma/upload/filter/mogrifun.ex
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Mogrifun do
+ @behaviour Pleroma.Upload.Filter
+ alias Pleroma.Upload.Filter
+
+ @moduledoc """
+ This module is just an example of an Upload filter. It's not supposed to be used in production.
+ """
+
+ @filters [
+ {"implode", "1"},
+ {"-raise", "20"},
+ {"+raise", "20"},
+ [{"-interpolate", "nearest"}, {"-virtual-pixel", "mirror"}, {"-spread", "5"}],
+ "+polaroid",
+ {"-statistic", "Mode 10"},
+ {"-emboss", "0x1.1"},
+ {"-emboss", "0x2"},
+ {"-colorspace", "Gray"},
+ "-negate",
+ [{"-channel", "green"}, "-negate"],
+ [{"-channel", "red"}, "-negate"],
+ [{"-channel", "blue"}, "-negate"],
+ {"+level-colors", "green,gold"},
+ {"+level-colors", ",DodgerBlue"},
+ {"+level-colors", ",Gold"},
+ {"+level-colors", ",Lime"},
+ {"+level-colors", ",Red"},
+ {"+level-colors", ",DarkGreen"},
+ {"+level-colors", "firebrick,yellow"},
+ {"+level-colors", "'rgb(102,75,25)',lemonchiffon"},
+ [{"fill", "red"}, {"tint", "40"}],
+ [{"fill", "green"}, {"tint", "40"}],
+ [{"fill", "blue"}, {"tint", "40"}],
+ [{"fill", "yellow"}, {"tint", "40"}]
+ ]
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, atom()} | {:error, String.t()}
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
+ try do
+ Filter.Mogrify.do_filter(file, [Enum.random(@filters)])
+ {:ok, :filtered}
+ rescue
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
+ end
+ end
+
+ def filter(_), do: {:ok, :noop}
+end
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
new file mode 100644
index 0000000..06efbf3
--- /dev/null
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -0,0 +1,48 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Mogrify do
+ @behaviour Pleroma.Upload.Filter
+
+ @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
+ @type conversions :: conversion() | [conversion()]
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, :atom} | {:error, String.t()}
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
+ try do
+ do_filter(file, Pleroma.Config.get!([__MODULE__, :args]))
+ {:ok, :filtered}
+ rescue
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
+ end
+ end
+
+ def filter(_), do: {:ok, :noop}
+
+ def do_filter(file, filters) do
+ file
+ |> Mogrify.open()
+ |> mogrify_filter(filters)
+ |> Mogrify.save(in_place: true)
+ end
+
+ defp mogrify_filter(mogrify, nil), do: mogrify
+
+ defp mogrify_filter(mogrify, [filter | rest]) do
+ mogrify
+ |> mogrify_filter(filter)
+ |> mogrify_filter(rest)
+ end
+
+ defp mogrify_filter(mogrify, []), do: mogrify
+
+ defp mogrify_filter(mogrify, {action, options}) do
+ Mogrify.custom(mogrify, action, options)
+ end
+
+ defp mogrify_filter(mogrify, action) when is_binary(action) do
+ Mogrify.custom(mogrify, action)
+ end
+end