summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/activity_pub/object_validators/common_validations.ex
blob: be50743481bc9c7d660e6435154e6c259239beb9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
  import Ecto.Changeset

  alias Pleroma.Activity
  alias Pleroma.Object
  alias Pleroma.User

  @spec validate_any_presence(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
  def validate_any_presence(cng, fields) do
    non_empty =
      fields
      |> Enum.map(fn field -> get_field(cng, field) end)
      |> Enum.any?(fn
        nil -> false
        [] -> false
        _ -> true
      end)

    if non_empty do
      cng
    else
      fields
      |> Enum.reduce(cng, fn field, cng ->
        cng
        |> add_error(field, "none of #{inspect(fields)} present")
      end)
    end
  end

  @spec validate_actor_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
  def validate_actor_presence(cng, options \\ []) do
    field_name = Keyword.get(options, :field_name, :actor)

    cng
    |> validate_change(field_name, fn field_name, actor ->
      case User.get_cached_by_ap_id(actor) do
        %User{is_active: false} ->
          [{field_name, "user is deactivated"}]

        %User{} ->
          []

        _ ->
          [{field_name, "can't find user"}]
      end
    end)
  end

  @spec validate_object_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
  def validate_object_presence(cng, options \\ []) do
    field_name = Keyword.get(options, :field_name, :object)
    allowed_types = Keyword.get(options, :allowed_types, false)

    cng
    |> validate_change(field_name, fn field_name, object_id ->
      object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)

      cond do
        !object ->
          [{field_name, "can't find object"}]

        object && allowed_types && object.data["type"] not in allowed_types ->
          [{field_name, "object not in allowed types"}]

        true ->
          []
      end
    end)
  end

  @spec validate_object_or_user_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
  def validate_object_or_user_presence(cng, options \\ []) do
    field_name = Keyword.get(options, :field_name, :object)
    options = Keyword.put(options, :field_name, field_name)

    actor_cng =
      cng
      |> validate_actor_presence(options)

    object_cng =
      cng
      |> validate_object_presence(options)

    if actor_cng.valid?, do: actor_cng, else: object_cng
  end

  @spec validate_host_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
  def validate_host_match(cng, fields \\ [:id, :actor]) do
    if same_domain?(cng, fields) do
      cng
    else
      fields
      |> Enum.reduce(cng, fn field, cng ->
        cng
        |> add_error(field, "hosts of #{inspect(fields)} aren't matching")
      end)
    end
  end

  @spec validate_fields_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
  def validate_fields_match(cng, fields) do
    if map_unique?(cng, fields) do
      cng
    else
      fields
      |> Enum.reduce(cng, fn field, cng ->
        cng
        |> add_error(field, "Fields #{inspect(fields)} aren't matching")
      end)
    end
  end

  defp map_unique?(cng, fields, func \\ & &1) do
    Enum.reduce_while(fields, nil, fn field, acc ->
      value =
        cng
        |> get_field(field)
        |> func.()

      case {value, acc} do
        {value, nil} -> {:cont, value}
        {value, value} -> {:cont, value}
        _ -> {:halt, false}
      end
    end)
  end

  @spec same_domain?(Ecto.Changeset.t(), [atom()]) :: boolean()
  def same_domain?(cng, fields \\ [:actor, :object]) do
    map_unique?(cng, fields, fn value -> URI.parse(value).host end)
  end

  # This figures out if a user is able to create, delete or modify something
  # based on the domain and superuser status
  @spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t()
  def validate_modification_rights(cng) do
    actor = User.get_cached_by_ap_id(get_field(cng, :actor))

    if User.superuser?(actor) || same_domain?(cng) do
      cng
    else
      cng
      |> add_error(:actor, "is not allowed to modify object")
    end
  end
end