diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex index 0cfab57e3..528fa14b5 100644 --- a/lib/federation/activity_pub/utils.ex +++ b/lib/federation/activity_pub/utils.ex @@ -25,11 +25,13 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. + @spec get_url(map() | String.t() | list(String.t()) | any()) :: String.t() | nil def get_url(%{"id" => id}), do: id def get_url(id) when is_binary(id), do: id def get_url(ids) when is_list(ids), do: get_url(hd(ids)) def get_url(_), do: nil + @spec make_json_ld_header :: map() def make_json_ld_header do %{ "@context" => [ @@ -99,6 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do } end + @spec make_date :: String.t() def make_date do DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() end @@ -130,6 +133,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do Applies to activities sent by group members from outside this instance to a group of this instance, we then need to relay (`Announce`) the object to other members on other instances. """ + @spec maybe_relay_if_group_activity(Activity.t(), Actor.t() | nil | list(Actor.t())) :: :ok def maybe_relay_if_group_activity(activity, attributed_to \\ nil) def maybe_relay_if_group_activity( @@ -153,6 +157,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do :ok end + @spec do_maybe_relay_if_group_activity(map(), list(String.t()) | String.t()) :: :ok defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to), do: do_maybe_relay_if_group_activity(object, hd(attributed_to)) @@ -199,6 +204,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do Adds an id and a published data if they aren't there, also adds it to an included object """ + @spec lazy_put_activity_defaults(map()) :: map() def lazy_put_activity_defaults(%{"object" => _object} = map) do if is_map(map["object"]) do object = lazy_put_object_defaults(map["object"]) @@ -215,6 +221,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do Map.put_new_lazy(map, "published", &make_date/0) end + @spec get_actor(map()) :: String.t() | nil def get_actor(%{"actor" => actor}) when is_binary(actor) do actor end @@ -242,6 +249,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do Takes the actor or attributedTo attributes (considers only the first elem if they're an array) """ + @spec origin_check?(String.t(), map()) :: boolean() def origin_check?(id, %{"type" => "Tombstone", "id" => tombstone_id}), do: id == tombstone_id def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params) @@ -283,6 +291,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do compare_uris?(uri_1, uri_2) end + @spec compare_uris?(URI.t(), URI.t()) :: boolean() defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host && id_uri.port == other_uri.port diff --git a/lib/graphql/error.ex b/lib/graphql/error.ex index e6b87b2c8..561567c5b 100644 --- a/lib/graphql/error.ex +++ b/lib/graphql/error.ex @@ -8,11 +8,19 @@ defmodule Mobilizon.GraphQL.Error do alias Mobilizon.Web.Gettext, as: GettextBackend import Mobilizon.Web.Gettext, only: [dgettext: 2] + @type t :: %{code: atom(), message: String.t(), status_code: pos_integer(), field: atom()} + defstruct [:code, :message, :status_code, :field] + @type error :: {:error, any()} | {:error, any(), any(), any()} | atom() + + @doc """ + Normalize an error to return `t`. + """ # Error Tuples # ------------ # Regular errors + @spec normalize(error | list(error) | String.t() | any()) :: t() def normalize({:error, reason}) do handle(reason) end diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex index a79ab2c5b..34703c765 100644 --- a/lib/mobilizon/actors/member.ex +++ b/lib/mobilizon/actors/member.ex @@ -8,18 +8,19 @@ defmodule Mobilizon.Actors.Member do import Ecto.Changeset alias Mobilizon.Actors.{Actor, MemberRole} + alias Mobilizon.Actors.Member.Metadata alias Mobilizon.Web.Endpoint @type t :: %__MODULE__{ role: MemberRole.t(), parent: Actor.t(), - actor: Actor.t() + actor: Actor.t(), + metadata: Metadata.t() } @required_attrs [:parent_id, :actor_id, :url] @optional_attrs [:role, :invited_by_id] @attrs @required_attrs ++ @optional_attrs - @metadata_attrs [] @primary_key {:id, :binary_id, autogenerate: true} schema "members" do @@ -27,9 +28,7 @@ defmodule Mobilizon.Actors.Member do field(:url, :string) field(:member_since, :utc_datetime) - embeds_one :metadata, Metadata, on_replace: :delete do - # TODO : Use this space to put notes when someone is invited / requested to join - end + embeds_one(:metadata, Metadata, on_replace: :delete) belongs_to(:invited_by, Actor) belongs_to(:parent, Actor) @@ -63,7 +62,7 @@ defmodule Mobilizon.Actors.Member do def changeset(%__MODULE__{} = member, attrs) do member |> cast(attrs, @attrs) - |> cast_embed(:metadata, with: &metadata_changeset/2) + |> cast_embed(:metadata) |> ensure_url() |> update_member_since() |> validate_required(@required_attrs) @@ -72,11 +71,6 @@ defmodule Mobilizon.Actors.Member do |> unique_constraint(:url, name: :members_url_index) end - defp metadata_changeset(schema, params) do - schema - |> cast(params, @metadata_attrs) - end - # If there's a blank URL that's because we're doing the first insert @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do diff --git a/lib/mobilizon/actors/member/metadata.ex b/lib/mobilizon/actors/member/metadata.ex new file mode 100644 index 000000000..ad4a251d1 --- /dev/null +++ b/lib/mobilizon/actors/member/metadata.ex @@ -0,0 +1,27 @@ +defmodule Mobilizon.Actors.Member.Metadata do + @moduledoc """ + Represents metadata on a membership + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{} + + @required_attrs [] + + @optional_attrs [] + + @attrs @required_attrs ++ @optional_attrs + + embedded_schema do + # TODO : Use this space to put notes when someone is invited / requested to join + end + + @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() + def changeset(schema, params) do + schema + |> cast(params, @attrs) + end +end diff --git a/lib/mobilizon/medias/media.ex b/lib/mobilizon/medias/media.ex index bda41a9d9..224252a52 100644 --- a/lib/mobilizon/medias/media.ex +++ b/lib/mobilizon/medias/media.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.Medias.Media do use Ecto.Schema - import Ecto.Changeset, only: [cast: 3, cast_embed: 2, cast_embed: 3] + import Ecto.Changeset, only: [cast: 3, cast_embed: 2] alias Mobilizon.Actors.Actor alias Mobilizon.Discussions.Comment @@ -20,16 +20,12 @@ defmodule Mobilizon.Medias.Media do actor: Actor.t() } - @metadata_attrs [:height, :width, :blurhash] + @attrs [:actor_id] schema "medias" do embeds_one(:file, File, on_replace: :update) - embeds_one :metadata, Metadata, on_replace: :update do - field(:height, :integer) - field(:width, :integer) - field(:blurhash, :string) - end + embeds_one(:metadata, Metadata, on_replace: :update) belongs_to(:actor, Actor) has_many(:event_picture, Event, foreign_key: :picture_id) @@ -45,15 +41,8 @@ defmodule Mobilizon.Medias.Media do @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(%__MODULE__{} = media, attrs) do media - |> cast(attrs, [:actor_id]) + |> cast(attrs, @attrs) |> cast_embed(:file) - |> cast_embed(:metadata, with: &metadata_changeset/2) - end - - @doc false - @spec metadata_changeset(Metadata.t(), map) :: Ecto.Changeset.t() - def metadata_changeset(metadata, attrs) do - metadata - |> cast(attrs, @metadata_attrs) + |> cast_embed(:metadata) end end diff --git a/lib/mobilizon/medias/media/metadata.ex b/lib/mobilizon/medias/media/metadata.ex new file mode 100644 index 000000000..b2d947da9 --- /dev/null +++ b/lib/mobilizon/medias/media/metadata.ex @@ -0,0 +1,38 @@ +defmodule Mobilizon.Medias.Media.Metadata do + @moduledoc """ + Represents a media metadata + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{ + width: non_neg_integer(), + height: non_neg_integer(), + blurhash: String.t() + } + + @required_attrs [] + + @optional_attrs [ + :width, + :height, + :blurhash + ] + + @attrs @required_attrs ++ @optional_attrs + + @primary_key false + embedded_schema do + field(:height, :integer) + field(:width, :integer) + field(:blurhash, :string) + end + + @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() + def changeset(schema, params) do + schema + |> cast(params, @attrs) + end +end diff --git a/lib/mobilizon/resources/resource.ex b/lib/mobilizon/resources/resource.ex index 5c1de9d37..83a84bfce 100644 --- a/lib/mobilizon/resources/resource.ex +++ b/lib/mobilizon/resources/resource.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Resources.Resource do import EctoEnum defenum(TypeEnum, folder: 0, link: 1, picture: 20, pad: 30, calc: 40, visio: 50) alias Mobilizon.Actors.Actor + alias Mobilizon.Resources.Resource.Metadata @type t :: %__MODULE__{ title: String.t(), @@ -17,7 +18,7 @@ defmodule Mobilizon.Resources.Resource do url: String.t(), resource_url: String.t(), type: atom(), - metadata: Mobilizon.Resources.Resource.Metadata.t(), + metadata: Metadata.t(), children: list(__MODULE__), parent: __MODULE__, actor: Actor.t(), @@ -37,20 +38,7 @@ defmodule Mobilizon.Resources.Resource do field(:local, :boolean, default: true) field(:published_at, :utc_datetime) - embeds_one :metadata, Metadata, on_replace: :delete do - field(:type, :string) - field(:title, :string) - field(:description, :string) - field(:image_remote_url, :string) - field(:width, :integer) - field(:height, :integer) - field(:author_name, :string) - field(:author_url, :string) - field(:provider_name, :string) - field(:provider_url, :string) - field(:html, :string) - field(:favicon_url, :string) - end + embeds_one(:metadata, Metadata, on_replace: :delete) has_many(:children, __MODULE__, foreign_key: :parent_id) belongs_to(:parent, __MODULE__, type: :binary_id) @@ -63,27 +51,13 @@ defmodule Mobilizon.Resources.Resource do @required_attrs [:title, :url, :actor_id, :creator_id, :type, :path, :published_at] @optional_attrs [:summary, :parent_id, :resource_url, :local] @attrs @required_attrs ++ @optional_attrs - @metadata_attrs [ - :type, - :title, - :description, - :image_remote_url, - :width, - :height, - :author_name, - :author_url, - :provider_name, - :provider_url, - :html, - :favicon_url - ] @doc false @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() def changeset(resource, attrs) do resource |> cast(attrs, @attrs) - |> cast_embed(:metadata, with: &metadata_changeset/2) + |> cast_embed(:metadata) |> ensure_url(:resource) |> maybe_add_published_at() |> validate_resource_or_folder() @@ -91,11 +65,6 @@ defmodule Mobilizon.Resources.Resource do |> unique_constraint(:url, name: :resource_url_index) end - defp metadata_changeset(schema, params) do - schema - |> cast(params, @metadata_attrs) - end - @spec validate_resource_or_folder(Changeset.t()) :: Changeset.t() defp validate_resource_or_folder(%Changeset{} = changeset) do with {status, type} when status in [:changes, :data] <- fetch_field(changeset, :type), diff --git a/lib/mobilizon/resources/resource/metadata.ex b/lib/mobilizon/resources/resource/metadata.ex new file mode 100644 index 000000000..f8f66c8a6 --- /dev/null +++ b/lib/mobilizon/resources/resource/metadata.ex @@ -0,0 +1,64 @@ +defmodule Mobilizon.Resources.Resource.Metadata do + @moduledoc """ + Represents a resource metadata + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{ + type: String.t(), + title: String.t(), + image_remote_url: String.t(), + width: non_neg_integer(), + height: non_neg_integer(), + author_name: String.t(), + author_url: String.t(), + provider_name: String.t(), + provider_url: String.t(), + html: String.t(), + favicon_url: String.t() + } + + @required_attrs [] + + @optional_attrs [ + :type, + :title, + :description, + :image_remote_url, + :width, + :height, + :author_name, + :author_url, + :provider_name, + :provider_url, + :html, + :favicon_url + ] + + @attrs @required_attrs ++ @optional_attrs + + @primary_key false + embedded_schema do + field(:type, :string) + field(:title, :string) + field(:description, :string) + field(:image_remote_url, :string) + field(:width, :integer) + field(:height, :integer) + field(:author_name, :string) + field(:author_url, :string) + field(:provider_name, :string) + field(:provider_url, :string) + field(:html, :string) + field(:favicon_url, :string) + end + + @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() + def changeset(schema, params) do + schema + |> cast(params, @attrs) + end +end diff --git a/lib/mobilizon/users/setting.ex b/lib/mobilizon/users/setting.ex index 9abb90754..6dab9b52d 100644 --- a/lib/mobilizon/users/setting.ex +++ b/lib/mobilizon/users/setting.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Users.Setting do use Ecto.Schema import Ecto.Changeset alias Mobilizon.Users.{NotificationPendingNotificationDelay, User} + alias Mobilizon.Users.Setting.Location @type t :: %__MODULE__{ timezone: String.t(), @@ -40,8 +41,6 @@ defmodule Mobilizon.Users.Setting do @attrs @required_attrs ++ @optional_attrs - @location_attrs [:name, :range, :geohash] - @primary_key {:user_id, :id, autogenerate: false} schema "user_settings" do field(:timezone, :string) @@ -60,11 +59,7 @@ defmodule Mobilizon.Users.Setting do field(:group_notifications, NotificationPendingNotificationDelay, default: :one_day) field(:last_notification_sent, :utc_datetime) - embeds_one :location, Location, on_replace: :update, primary_key: false do - field(:name, :string) - field(:range, :integer) - field(:geohash, :string) - end + embeds_one(:location, Location, on_replace: :update) belongs_to(:user, User, primary_key: true, type: :id, foreign_key: :id, define_field: false) @@ -76,13 +71,7 @@ defmodule Mobilizon.Users.Setting do def changeset(setting, attrs) do setting |> cast(attrs, @attrs) - |> cast_embed(:location, with: &location_changeset/2) + |> cast_embed(:location) |> validate_required(@required_attrs) end - - @spec location_changeset(location, map) :: Ecto.Changeset.t() - def location_changeset(schema, params) do - schema - |> cast(params, @location_attrs) - end end diff --git a/lib/mobilizon/users/setting/location.ex b/lib/mobilizon/users/setting/location.ex new file mode 100644 index 000000000..c403e2da6 --- /dev/null +++ b/lib/mobilizon/users/setting/location.ex @@ -0,0 +1,38 @@ +defmodule Mobilizon.Users.Setting.Location do + @moduledoc """ + Represents user location information + """ + + use Ecto.Schema + import Ecto.Changeset + + @type t :: %__MODULE__{ + name: String.t(), + range: non_neg_integer(), + geohash: String.t() + } + + @required_attrs [] + + @optional_attrs [ + :name, + :range, + :geohash + ] + + @attrs @required_attrs ++ @optional_attrs + + @primary_key false + embedded_schema do + field(:name, :string) + field(:range, :integer) + field(:geohash, :string) + end + + @doc false + @spec changeset(t | Ecto.Schema.t(), map) :: Ecto.Changeset.t() + def changeset(schema, params) do + schema + |> cast(params, @attrs) + end +end diff --git a/lib/web/plugs/http_security_plug.ex b/lib/web/plugs/http_security_plug.ex index 8a278074d..938242462 100644 --- a/lib/web/plugs/http_security_plug.ex +++ b/lib/web/plugs/http_security_plug.ex @@ -62,9 +62,9 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do static_url = Mobilizon.Web.Endpoint.static_url() websocket_url = Mobilizon.Web.Endpoint.websocket_url() - img_src = [@img_src | get_csp_config(:img_src, options)] + img_src = [@img_src] ++ [get_csp_config(:img_src, options)] - media_src = [@media_src | get_csp_config(:media_src, options)] + media_src = [@media_src] ++ [get_csp_config(:media_src, options)] connect_src = [ @connect_src, @@ -85,22 +85,22 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do ] end - script_src = [script_src | get_csp_config(:script_src, options)] + script_src = [script_src] ++ [get_csp_config(:script_src, options)] style_src = if Config.get(:env) == :dev, do: [@style_src | "'unsafe-inline' "], else: @style_src - style_src = [style_src | get_csp_config(:style_src, options)] + style_src = [style_src] ++ [get_csp_config(:style_src, options)] - font_src = [@font_src | get_csp_config(:font_src, options)] + font_src = [@font_src] ++ [get_csp_config(:font_src, options)] frame_src = if Config.get(:env) == :dev, do: "frame-src 'self' ", else: "frame-src 'none' " - frame_src = [frame_src | get_csp_config(:frame_src, options)] + frame_src = [frame_src] ++ [get_csp_config(:frame_src, options)] frame_ancestors = if Config.get(:env) == :dev, do: "frame-ancestors 'self' ", else: "frame-ancestors 'none' " - frame_ancestors = [frame_ancestors | get_csp_config(:frame_ancestors, options)] + frame_ancestors = [frame_ancestors] ++ [get_csp_config(:frame_ancestors, options)] insecure = if scheme == "https", do: "upgrade-insecure-requests" diff --git a/lib/web/plugs/set_locale_plug.ex b/lib/web/plugs/set_locale_plug.ex index 0af554e13..fbfc371ea 100644 --- a/lib/web/plugs/set_locale_plug.ex +++ b/lib/web/plugs/set_locale_plug.ex @@ -11,8 +11,10 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do import Plug.Conn, only: [assign: 3] alias Mobilizon.Web.Gettext, as: GettextBackend + @spec init(any()) :: nil def init(_), do: nil + @spec call(Plug.Conn.t(), any()) :: Plug.Conn.t() def call(conn, _) do locale = [ @@ -29,17 +31,22 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do assign(conn, :locale, locale) end + @spec supported_locale?(String.t()) :: boolean() defp supported_locale?(locale) do GettextBackend |> Gettext.known_locales() |> Enum.member?(locale) end + @spec default_locale :: String.t() defp default_locale do Keyword.get(Mobilizon.Config.instance_config(), :default_language, "en") end - @spec determine_best_locale(String.t()) :: String.t() + @doc """ + Determine the best available locale for a given locale ID + """ + @spec determine_best_locale(String.t()) :: String.t() | nil def determine_best_locale(locale) when is_binary(locale) do locale = String.trim(locale) locales = Gettext.known_locales(GettextBackend) @@ -58,5 +65,6 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do def determine_best_locale(_), do: nil # Keep only the first part of the locale + @spec split_locale(String.t()) :: String.t() defp split_locale(locale), do: locale |> String.split("_", trim: true, parts: 2) |> hd end diff --git a/lib/web/proxy/reverse_proxy.ex b/lib/web/proxy/reverse_proxy.ex index 766dd5e06..9bb83b5a7 100644 --- a/lib/web/proxy/reverse_proxy.ex +++ b/lib/web/proxy/reverse_proxy.ex @@ -165,6 +165,11 @@ defmodule Mobilizon.Web.ReverseProxy do |> halt() end + @spec request(String.t(), String.t(), list(tuple()), Keyword.t()) :: + {:ok, 200 | 206 | 304, list(tuple()), any()} + | {:ok, 200 | 206 | 304, list(tuple())} + | {:error, {:invalid_http_response, pos_integer()}} + | {:error, any()} defp request(method, url, headers, hackney_opts) do Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") method = method |> String.downcase() |> String.to_existing_atom() @@ -184,6 +189,8 @@ defmodule Mobilizon.Web.ReverseProxy do end end + @spec response(Plug.Conn.t(), any(), String.t(), pos_integer(), list(tuple()), Keyword.t()) :: + Plug.Conn.t() defp response(conn, client, url, status, headers, opts) do result = conn @@ -209,18 +216,26 @@ defmodule Mobilizon.Web.ReverseProxy do end end + @spec chunk_reply( + Plug.Conn.t(), + any(), + Keyword.t(), + non_neg_integer(), + non_neg_integer() | :no_duration_limit + ) :: + {:ok, Plug.Conn.t()} | {:error, any(), Plug.Conn.t()} defp chunk_reply(conn, client, opts) do chunk_reply(conn, client, opts, 0, 0) end defp chunk_reply(conn, client, opts, sent_so_far, duration) do - with {:ok, duration} <- + with {:ok, {duration, now}} <- check_read_duration( duration, Keyword.get(opts, :max_read_duration, @max_read_duration) ), {:ok, data} <- @hackney.stream_body(client), - {:ok, duration} <- increase_read_duration(duration), + {:ok, duration} <- increase_read_duration({duration, now}), sent_so_far = sent_so_far + byte_size(data), :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), {:ok, conn} <- chunk(conn, data) do @@ -231,6 +246,8 @@ defmodule Mobilizon.Web.ReverseProxy do end end + @spec head_response(Plug.Conn.t(), any(), pos_integer(), list(tuple()), Keyword.t()) :: + Plug.Conn.t() | no_return() defp head_response(conn, _url, code, headers, opts) do conn |> put_resp_headers(build_resp_headers(headers, opts)) @@ -238,6 +255,8 @@ defmodule Mobilizon.Web.ReverseProxy do end # sobelow_skip ["XSS.SendResp"] + @spec error_or_redirect(Plug.Conn.t(), String.t(), pos_integer(), String.t(), Keyword.t()) :: + Plug.Conn.t() defp error_or_redirect(conn, url, code, body, opts) do if Keyword.get(opts, :redirect_on_failure, false) do conn @@ -250,12 +269,14 @@ defmodule Mobilizon.Web.ReverseProxy do end end + @spec downcase_headers(list(tuple())) :: list(tuple()) defp downcase_headers(headers) do Enum.map(headers, fn {k, v} -> {String.downcase(k), v} end) end + @spec get_content_type(list(tuple())) :: String.t() defp get_content_type(headers) do {_, content_type} = List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"}) @@ -264,12 +285,14 @@ defmodule Mobilizon.Web.ReverseProxy do content_type end + @spec put_resp_headers(Plug.Conn.t(), list(tuple())) :: Plug.Conn.t() defp put_resp_headers(conn, headers) do Enum.reduce(headers, conn, fn {k, v}, conn -> put_resp_header(conn, k, v) end) end + @spec build_req_headers(list(tuple()), Keyword.t()) :: list(tuple()) defp build_req_headers(headers, opts) do headers |> downcase_headers() @@ -290,6 +313,7 @@ defmodule Mobilizon.Web.ReverseProxy do end).() end + @spec build_resp_headers(list(tuple()), Keyword.t()) :: list(tuple()) defp build_resp_headers(headers, opts) do headers |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) @@ -298,6 +322,7 @@ defmodule Mobilizon.Web.ReverseProxy do |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).() end + @spec build_resp_cache_headers(list(tuple()), Keyword.t()) :: list(tuple()) defp build_resp_cache_headers(headers, _opts) do has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) has_cache_control? = List.keymember?(headers, "cache-control", 0) @@ -321,6 +346,7 @@ defmodule Mobilizon.Web.ReverseProxy do end end + @spec build_resp_content_disposition_header(list(tuple()), Keyword.t()) :: list(tuple()) defp build_resp_content_disposition_header(headers, opts) do opt = Keyword.get(opts, :inline_content_types, @inline_content_types) @@ -359,6 +385,8 @@ defmodule Mobilizon.Web.ReverseProxy do end end + @spec header_length_constraint(list(tuple()), non_neg_integer()) :: + :ok | {:error, :body_too_large} defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do with {_, size} <- List.keyfind(headers, "content-length", 0), {size, _} <- Integer.parse(size), @@ -375,15 +403,16 @@ defmodule Mobilizon.Web.ReverseProxy do defp header_length_constraint(_, _), do: :ok + @spec body_size_constraint(integer(), integer()) :: :ok | {:error, :body_too_large} defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do {:error, :body_too_large} end defp body_size_constraint(_, _), do: :ok - @spec check_read_duration(any(), integer()) :: + @spec check_read_duration(any(), integer() | :no_duration_limit) :: {:ok, {integer(), integer()}} - | {:ok, :no_duration_limit, :no_duration_limit} + | {:ok, {:no_duration_limit, :no_duration_limit}} | {:error, :read_duration_exceeded} defp check_read_duration(duration, max) when is_integer(duration) and is_integer(max) and max > 0 do @@ -394,8 +423,12 @@ defmodule Mobilizon.Web.ReverseProxy do end end - defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit} + defp check_read_duration(_, _), do: {:ok, {:no_duration_limit, :no_duration_limit}} + @spec increase_read_duration( + {previous_duration :: pos_integer | :no_duration_limit, + started :: pos_integer | :no_duration_limit} + ) :: {:ok, pos_integer()} | {:ok, :no_duration_limit} defp increase_read_duration({previous_duration, started}) when is_integer(previous_duration) and is_integer(started) do duration = :erlang.system_time(:millisecond) - started @@ -403,9 +436,10 @@ defmodule Mobilizon.Web.ReverseProxy do end defp increase_read_duration(_) do - {:ok, :no_duration_limit, :no_duration_limit} + {:ok, :no_duration_limit} end + @spec filename(String.t()) :: String.t() | nil def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex index eba9dd8a3..3e0f61930 100644 --- a/lib/web/views/activity_pub/actor_view.ex +++ b/lib/web/views/activity_pub/actor_view.ex @@ -17,6 +17,7 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do @json_ld_header Utils.make_json_ld_header() @selected_member_roles ~w(creator administrator moderator member)a + @spec render(String.t(), map()) :: map() def render("actor.json", %{actor: actor}) do actor |> Convertible.model_to_as() diff --git a/lib/web/views/activity_pub/object_view.ex b/lib/web/views/activity_pub/object_view.ex index 9ababb1c3..a3ee04ec8 100644 --- a/lib/web/views/activity_pub/object_view.ex +++ b/lib/web/views/activity_pub/object_view.ex @@ -3,6 +3,7 @@ defmodule Mobilizon.Web.ActivityPub.ObjectView do alias Mobilizon.Federation.ActivityPub.{Activity, Utils} + @spec render(String.t(), map()) :: map() def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do %{ "id" => data["id"], diff --git a/lib/web/views/auth_view.ex b/lib/web/views/auth_view.ex index 67079bd8e..9ec29ff29 100644 --- a/lib/web/views/auth_view.ex +++ b/lib/web/views/auth_view.ex @@ -8,6 +8,7 @@ defmodule Mobilizon.Web.AuthView do alias Phoenix.HTML.Tag import Mobilizon.Web.Views.Utils + @spec render(String.t(), map()) :: String.t() | Plug.Conn.t() def render("callback.html", %{ conn: conn, access_token: access_token, diff --git a/lib/web/views/error_helpers.ex b/lib/web/views/error_helpers.ex index 5a42c31c2..eb63a4d26 100644 --- a/lib/web/views/error_helpers.ex +++ b/lib/web/views/error_helpers.ex @@ -8,6 +8,7 @@ defmodule Mobilizon.Web.ErrorHelpers do @doc """ Translates an error message using gettext. """ + @spec translate_error({msg :: String.t(), opts :: map()}) :: String.t() def translate_error({msg, opts}) do # Because error messages were defined within Ecto, we must # call the Gettext module passing our Gettext backend. We diff --git a/lib/web/views/error_view.ex b/lib/web/views/error_view.ex index de8e7a704..28e8096e3 100644 --- a/lib/web/views/error_view.ex +++ b/lib/web/views/error_view.ex @@ -6,6 +6,7 @@ defmodule Mobilizon.Web.ErrorView do alias Mobilizon.Service.Metadata.Instance import Mobilizon.Web.Views.Utils + @spec render(String.t(), map()) :: map() | String.t() | Plug.Conn.t() def render("404.html", %{conn: conn}) do with tags <- Instance.build_tags(), {:ok, html} <- inject_tags(tags, get_locale(conn)) do diff --git a/lib/web/views/json_ld/object_view.ex b/lib/web/views/json_ld/object_view.ex index 333193a06..cc41e511b 100644 --- a/lib/web/views/json_ld/object_view.ex +++ b/lib/web/views/json_ld/object_view.ex @@ -8,6 +8,7 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do alias Mobilizon.Web.Endpoint alias Mobilizon.Web.JsonLD.ObjectView + @spec render(String.t(), map()) :: map() def render("group.json", %{group: %Actor{} = group}) do %{ "@context" => "http://schema.org", @@ -93,6 +94,7 @@ defmodule Mobilizon.Web.JsonLD.ObjectView do } end + @spec render_location(map()) :: map() | nil defp render_location(%{physical_address: %Address{} = address}), do: render_one(address, ObjectView, "place.json", as: :address) diff --git a/lib/web/views/page_view.ex b/lib/web/views/page_view.ex index 3ce02182b..7c040bd9a 100644 --- a/lib/web/views/page_view.ex +++ b/lib/web/views/page_view.ex @@ -19,6 +19,8 @@ defmodule Mobilizon.Web.PageView do alias Mobilizon.Federation.ActivityStream.Convertible import Mobilizon.Web.Views.Utils + @doc false + @spec render(String.t(), %{conn: Plug.Conn.t()}) :: map() | String.t() | Plug.Conn.t() def render("actor.activity-json", %{conn: %{assigns: %{object: %Actor{} = actor}}}) do actor |> Convertible.model_to_as() diff --git a/test/support/factory.ex b/test/support/factory.ex index 953ac8c0b..4294ffcd7 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -11,6 +11,7 @@ defmodule Mobilizon.Factory do alias Mobilizon.Web.{Endpoint, Upload} alias Mobilizon.Web.Router.Helpers, as: Routes + @spec user_factory :: Mobilizon.Users.User.t() def user_factory do %Mobilizon.Users.User{ password_hash: "Jane Smith", @@ -23,6 +24,7 @@ defmodule Mobilizon.Factory do } end + @spec settings_factory :: Mobilizon.Users.Setting.t() def settings_factory do %Mobilizon.Users.Setting{ timezone: nil, @@ -37,6 +39,7 @@ defmodule Mobilizon.Factory do } end + @spec actor_factory :: Mobilizon.Actors.Actor.t() def actor_factory do preferred_username = sequence("thomas") @@ -62,6 +65,7 @@ defmodule Mobilizon.Factory do } end + @spec group_factory :: Mobilizon.Actors.Actor.t() def group_factory do preferred_username = sequence("myGroup") @@ -82,6 +86,7 @@ defmodule Mobilizon.Factory do ) end + @spec instance_actor_factory :: Mobilizon.Actors.Actor.t() def instance_actor_factory do preferred_username = "relay" domain = "#{sequence("mydomain")}.com" @@ -104,6 +109,7 @@ defmodule Mobilizon.Factory do ) end + @spec follower_factory :: Mobilizon.Actors.Follower.t() def follower_factory do uuid = Ecto.UUID.generate() @@ -116,6 +122,7 @@ defmodule Mobilizon.Factory do } end + @spec tag_factory :: Mobilizon.Events.Tag.t() def tag_factory do %Mobilizon.Events.Tag{ title: sequence("MyTag"), @@ -123,6 +130,7 @@ defmodule Mobilizon.Factory do } end + @spec tag_relation_factory :: Mobilizon.Events.TagRelation.t() def tag_relation_factory do %Mobilizon.Events.TagRelation{ tag: build(:tag), @@ -130,6 +138,7 @@ defmodule Mobilizon.Factory do } end + @spec address_factory :: Mobilizon.Addresses.Address.t() def address_factory do %Mobilizon.Addresses.Address{ description: sequence("MyAddress"), @@ -143,6 +152,7 @@ defmodule Mobilizon.Factory do } end + @spec comment_factory :: Mobilizon.Discussions.Comment.t() def comment_factory do uuid = Ecto.UUID.generate() @@ -165,6 +175,7 @@ defmodule Mobilizon.Factory do } end + @spec event_factory :: Mobilizon.Events.Event.t() def event_factory do actor = build(:actor) start = Timex.shift(DateTime.utc_now(), hours: 2) @@ -198,6 +209,7 @@ defmodule Mobilizon.Factory do } end + @spec participant_factory :: Mobilizon.Events.Participant.t() def participant_factory do uuid = Ecto.UUID.generate() @@ -214,6 +226,7 @@ defmodule Mobilizon.Factory do } end + @spec session_factory :: Mobilizon.Events.Session.t() def session_factory do %Mobilizon.Events.Session{ title: sequence("MySession"), @@ -222,6 +235,7 @@ defmodule Mobilizon.Factory do } end + @spec track_factory :: Mobilizon.Events.Track.t() def track_factory do %Mobilizon.Events.Track{ name: sequence("MyTrack"), @@ -229,6 +243,7 @@ defmodule Mobilizon.Factory do } end + @spec bot_factory :: Mobilizon.Actors.Bot.t() def bot_factory do %Mobilizon.Actors.Bot{ source: "https://mysource.tld/feed.ics", @@ -238,6 +253,7 @@ defmodule Mobilizon.Factory do } end + @spec member_factory :: Mobilizon.Actors.Member.t() def member_factory do uuid = Ecto.UUID.generate() @@ -250,6 +266,7 @@ defmodule Mobilizon.Factory do } end + @spec feed_token_factory :: Mobilizon.Events.FeedToken.t() def feed_token_factory do user = build(:user) @@ -260,6 +277,7 @@ defmodule Mobilizon.Factory do } end + @spec file_factory :: Mobilizon.Medias.File.t() def file_factory do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") @@ -285,6 +303,7 @@ defmodule Mobilizon.Factory do } end + @spec media_factory :: Mobilizon.Medias.Media.t() def media_factory do %Mobilizon.Medias.Media{ file: build(:file), @@ -292,6 +311,7 @@ defmodule Mobilizon.Factory do } end + @spec report_factory :: Mobilizon.Reports.Report.t() def report_factory do %Mobilizon.Reports.Report{ content: "This is problematic", @@ -304,6 +324,7 @@ defmodule Mobilizon.Factory do } end + @spec report_note_factory :: Mobilizon.Reports.Note.t() def report_note_factory do %Mobilizon.Reports.Note{ content: "My opinion", @@ -312,6 +333,7 @@ defmodule Mobilizon.Factory do } end + @spec todo_list_factory :: Mobilizon.Todos.TodoList.t() def todo_list_factory do uuid = Ecto.UUID.generate() @@ -324,6 +346,7 @@ defmodule Mobilizon.Factory do } end + @spec todo_factory :: Mobilizon.Todos.Todo.t() def todo_factory do uuid = Ecto.UUID.generate() @@ -340,6 +363,7 @@ defmodule Mobilizon.Factory do } end + @spec resource_factory :: Mobilizon.Resources.Resource.t() def resource_factory do uuid = Ecto.UUID.generate() title = sequence("my resource") @@ -358,6 +382,7 @@ defmodule Mobilizon.Factory do } end + @spec admin_setting_factory :: Mobilizon.Admin.Setting.t() def admin_setting_factory do %Mobilizon.Admin.Setting{ group: sequence("group"), @@ -366,6 +391,7 @@ defmodule Mobilizon.Factory do } end + @spec post_factory :: Mobilizon.Posts.Post.t() def post_factory do uuid = Ecto.UUID.generate() @@ -386,6 +412,7 @@ defmodule Mobilizon.Factory do } end + @spec tombstone_factory :: Mobilizon.Tombstone.t() def tombstone_factory do uuid = Ecto.UUID.generate() @@ -395,6 +422,7 @@ defmodule Mobilizon.Factory do } end + @spec discussion_factory :: Mobilizon.Discussions.Discussion.t() def discussion_factory do uuid = Ecto.UUID.generate() actor = build(:actor) @@ -413,6 +441,7 @@ defmodule Mobilizon.Factory do } end + @spec mobilizon_activity_factory :: Mobilizon.Activities.Activity.t() def mobilizon_activity_factory do group = build(:group) actor = build(:actor) @@ -433,6 +462,7 @@ defmodule Mobilizon.Factory do } end + @spec mobilizon_activity_setting_factory :: Mobilizon.Users.ActivitySetting.t() def mobilizon_activity_setting_factory do %Mobilizon.Users.ActivitySetting{ key: "event_created", @@ -442,6 +472,7 @@ defmodule Mobilizon.Factory do } end + @spec push_subscription_factory :: Mobilizon.Users.PushSubscription.t() def push_subscription_factory do %Mobilizon.Users.PushSubscription{ digest: "", @@ -452,6 +483,7 @@ defmodule Mobilizon.Factory do } end + @spec share_factory :: Mobilizon.Share.t() def share_factory do %Mobilizon.Share{ actor: build(:actor), @@ -460,6 +492,7 @@ defmodule Mobilizon.Factory do } end + @spec event_metadata_factory :: Mobilizon.Events.EventMetadata.t() def event_metadata_factory do %Mobilizon.Events.EventMetadata{ key: sequence("mz:custom:something"),