defmodule Image.Plug.Provider.Imgix do @moduledoc """ Imgix URL provider. Wires `Image.Plug.Provider.Imgix.URL` (URL recognition), `Image.Plug.Provider.Imgix.Signing` (optional HMAC verification), and `Image.Plug.Provider.Imgix.Options` (option parsing) into the `Image.Plug.Provider` behaviour. Mount under any path (typically the root of an imgix-style subdomain). Every request under the mount is treated as a transform request whose options come from the query string. ### Options * `:mount` — string path prefix the plug is mounted under. Stripped before treating the rest as the source path. Defaults to `""`. * `:strict?` — `true` (default) rejects unknown imgix option keys with `:unknown_option`. `false` logs and ignores them. * `:signing` — `nil` (default; no signing) or `%{keys: [...], required?: bool}`. When set, every request URL must carry a valid `?s=` parameter. Wire format matches imgix's hosted signed URLs. See `Image.Plug.Provider.Imgix.Signing`. ### URL forms recognised * `/?` — web-folder source. * `/?` — web-proxy source. ### Conformance See `guides/imgix_conformance.md` for the per-option support matrix and the documented gaps. """ @behaviour Image.Plug.Provider alias Image.Plug.Error alias Image.Plug.Provider.Imgix.{Options, Signing, URL} @impl Image.Plug.Provider def parse(%Plug.Conn{} = conn, options) when is_list(options) do with :ok <- verify_signature(conn, options), {:ok, %{shape: :imgix, options: query_string, source: source}} <- URL.parse(conn, Keyword.take(options, [:mount])), {:ok, pipeline} <- Options.parse(query_string, Keyword.take(options, [:strict?])) do result = cond do pipeline.ops != [] -> {:pipeline, pipeline, source} non_default_output?(pipeline.output) -> {:pipeline, pipeline, source} true -> {:passthrough, source} end {:ok, result} end end defp verify_signature(conn, options) do case Keyword.get(options, :signing) do nil -> :ok %{keys: [_ | _] = keys} = signing -> Signing.verify( request_path_with_query(conn), keys, required?: Map.get(signing, :required?, false) ) other -> {:error, Error.new( :invalid_option, "imgix :signing must be nil or %{keys: [...], required?: bool}", details: %{got: inspect(other)} )} end end defp request_path_with_query(%Plug.Conn{request_path: path, query_string: ""}), do: path defp request_path_with_query(%Plug.Conn{request_path: path, query_string: q}), do: "#{path}?#{q}" # The default Format struct represents "no output options set". # Anything else means the request asked for a specific format, # quality, metadata policy, etc. — even if no transform ops were # parsed — and we should run through the encoder rather than # passthrough. defp non_default_output?(%Image.Plug.Pipeline.Ops.Format{} = format) do format != %Image.Plug.Pipeline.Ops.Format{} end end