2.2 KiB · text History b868907
defmodule GitGudWeb.AvatarController do
  @moduledoc """
  Serves avatars for users, organizations and repositories.

      GET /u/:handle/avatar
      GET /orgs/:handle/avatar
      GET /r/:owner/:name/avatar

  Always an image. An owner with no upload gets the generated
  identicon, which is what the profile page has always drawn — so a
  caller never has to handle "no avatar", and an `<img>` never has a
  broken state.

  Unauthenticated on purpose. An avatar is decoration attached to a
  handle that is already public, and requiring a session would break
  every `<img>` in a federated context or an email.
  """
  use GitGudWeb, :controller

  alias GitGud.Avatars
  alias GitGud.Repositories

  # A year. Safe because the URL carries the content digest — a
  # replaced avatar is a different URL, so nothing has to expire for
  # the change to be seen.
  @immutable_max_age 31_536_000

  def user(conn, %{"handle" => handle}) do
    case Repositories.resolve_namespace(handle) do
      {:ok, owner} -> send_avatar(conn, owner)
      :error -> send_resp(conn, 404, "")
    end
  end

  def organization(conn, params), do: user(conn, params)

  def repository(conn, %{"owner" => owner, "name" => name}) do
    case Repositories.find_by_path(owner, name) do
      nil -> send_resp(conn, 404, "")
      repo -> send_avatar(conn, repo)
    end
  end

  defp send_avatar(conn, owner) do
    {:ok, content_type, body} = Avatars.render(owner)

    conn
    |> put_resp_content_type(content_type)
    |> put_resp_header("cache-control", cache_control(conn))
    # Belt and braces on a path that serves user-supplied bytes: the
    # type is sniffed from the body on upload, but a browser that
    # decides to sniff again shouldn't be able to reach a different
    # conclusion.
    |> put_resp_header("x-content-type-options", "nosniff")
    |> send_resp(200, body)
  end

  # Only immutable when the request carried a version. A bare URL has
  # to stay revalidatable, or a visitor who followed a link without one
  # is stuck with a stale face for a year.
  defp cache_control(conn) do
    case conn.query_params do
      %{"v" => v} when is_binary(v) and v != "" ->
        "public, max-age=#{@immutable_max_age}, immutable"

      _ ->
        "public, max-age=300"
    end
  end
end