Merge branch 'remove-swift' into 'develop'
authorkaniini <nenolod@gmail.com>
Tue, 9 Jul 2019 16:54:32 +0000 (16:54 +0000)
committerkaniini <nenolod@gmail.com>
Tue, 9 Jul 2019 16:54:32 +0000 (16:54 +0000)
Remove Uploaders.Swift and `httpoison` dependency

Closes #1051

See merge request pleroma/pleroma!1374

50 files changed:
CHANGELOG.md
config/config.exs
config/test.exs
docs/config.md
lib/mix/tasks/pleroma/ecto/ecto.ex
lib/mix/tasks/pleroma/instance.ex
lib/pleroma/object.ex
lib/pleroma/object/fetcher.ex
lib/pleroma/reverse_proxy/client.ex [new file with mode: 0644]
lib/pleroma/reverse_proxy/reverse_proxy.ex [moved from lib/pleroma/reverse_proxy.ex with 97% similarity]
lib/pleroma/user.ex
lib/pleroma/user/search.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/federator/federator.ex
lib/pleroma/web/mastodon_api/search_controller.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/mastodon_api/websocket_handler.ex
lib/pleroma/web/media_proxy/media_proxy.ex
lib/pleroma/web/metadata/opengraph.ex
lib/pleroma/web/metadata/twitter_card.ex
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
lib/pleroma/web/ostatus/handlers/note_handler.ex
lib/pleroma/web/ostatus/ostatus.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock
priv/repo/migrations/20170522160642_case_insensivtivity.exs
rel/files/bin/pleroma_ctl
test/conversation_test.exs
test/http/request_builder_test.exs [new file with mode: 0644]
test/integration/mastodon_websocket_test.exs
test/media_proxy_test.exs
test/reverse_proxy_test.exs [new file with mode: 0644]
test/support/helpers.ex
test/tasks/ecto/ecto_test.exs [new file with mode: 0644]
test/tasks/pleroma_test.exs [new file with mode: 0644]
test/tasks/robots_txt_test.exs [new file with mode: 0644]
test/test_helper.exs
test/user_search_test.exs [new file with mode: 0644]
test/user_test.exs
test/web/activity_pub/activity_pub_controller_test.exs
test/web/activity_pub/transmogrifier_test.exs
test/web/federator_test.exs
test/web/mastodon_api/status_view_test.exs
test/web/ostatus/ostatus_controller_test.exs
test/web/ostatus/ostatus_test.exs
test/web/plugs/federating_plug_test.exs
test/web/web_finger/web_finger_controller_test.exs
test/web/websub/websub_controller_test.exs

index 8a0dad4534fd44488392413c7a47c9576f257ab7..75fa50e000e7fbf69099e5ae5388556e8b1ea2a0 100644 (file)
@@ -6,16 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ## [Unreleased]
 ### Added
 - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
+Configuration: `federation_incoming_replies_max_depth` option
 - Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
 - Admin API: Return users' tags when querying reports
 - Admin API: Return avatar and display name when querying users
+- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
 
 ### Fixed
 - Not being able to pin unlisted posts
+- Metadata rendering errors resulting in the entire page being inaccessible
+- Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
+- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
 
 ### Changed
+- Configuration: OpenGraph and TwitterCard providers enabled by default
 - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
 
+### Changed
+- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
+
 ## [1.0.0] - 2019-06-29
 ### Security
 - Mastodon API: Fix display names not being sanitized
index e337f00aaa02a65d46d73b21741db7bfb6f3bfdd..675fbb5515996acf356845cda3264805581f6fcd 100644 (file)
@@ -218,6 +218,7 @@ config :pleroma, :instance,
   },
   registrations_open: true,
   federating: true,
+  federation_incoming_replies_max_depth: 100,
   federation_reachability_timeout_days: 7,
   federation_publisher_modules: [
     Pleroma.Web.ActivityPub.Publisher,
@@ -358,7 +359,11 @@ config :pleroma, :gopher,
   port: 9999
 
 config :pleroma, Pleroma.Web.Metadata,
-  providers: [Pleroma.Web.Metadata.Providers.RelMe],
+  providers: [
+    Pleroma.Web.Metadata.Providers.OpenGraph,
+    Pleroma.Web.Metadata.Providers.TwitterCard,
+    Pleroma.Web.Metadata.Providers.RelMe
+  ],
   unfurl_nsfw: false
 
 config :pleroma, :suggestions,
index 9d441a7f5e5123ebb80af51eca4544dcd3116d23..63443dde0115a1127df6133a645b329a01810be4 100644 (file)
@@ -28,7 +28,8 @@ config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test
 config :pleroma, :instance,
   email: "admin@example.com",
   notify_email: "noreply@example.com",
-  skip_thread_containment: false
+  skip_thread_containment: false,
+  federating: false
 
 # Configure your database
 config :pleroma, Pleroma.Repo,
@@ -74,6 +75,8 @@ rum_enabled = System.get_env("RUM_ENABLED") == "true"
 config :pleroma, :database, rum_enabled: rum_enabled
 IO.puts("RUM enabled: #{rum_enabled}")
 
+config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
+
 try do
   import_config "test.secret.exs"
 rescue
index 8afccb228d7ca9991fd4998d365a2b71102eada8..822c34c515a3e7cfbf301dcfe20d8914a3f60d4b 100644 (file)
@@ -87,6 +87,7 @@ config :pleroma, Pleroma.Emails.Mailer,
 * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
 * `account_activation_required`: Require users to confirm their emails before signing in.
 * `federating`: Enable federation with other instances
+* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
 * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
 * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
 * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
index 324f57fdd789be719ca6ea3fd8b386f922c59e3b..b66f6337643b011cb20c43c489f46c4fdc2a187b 100644 (file)
@@ -1,6 +1,7 @@
 # Pleroma: A lightweight social networking server
 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-onl
+
 defmodule Mix.Tasks.Pleroma.Ecto do
   @doc """
   Ensures the given repository's migrations path exists on the file system.
index a27c4b897fe0e9aa506b313b7f1237de4eef1c91..2ae16adc0d73378f59a95364d3fbd26cb9ab8613 100644 (file)
@@ -149,7 +149,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
       uploads_dir =
         get_option(
           options,
-          :upload_dir,
+          :uploads_dir,
           "What directory should media uploads go in (when using the local uploader)?",
           Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
         )
index 4b181ec59f0ab225d3c1c44add562587036cf226..b8647dd26ed9f8aa9bc9a2f661e8eee34a28525c 100644 (file)
@@ -44,20 +44,20 @@ defmodule Pleroma.Object do
     Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
   end
 
-  def normalize(_, fetch_remote \\ true)
+  def normalize(_, fetch_remote \\ true, options \\ [])
   # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
   # Use this whenever possible, especially when walking graphs in an O(N) loop!
-  def normalize(%Object{} = object, _), do: object
-  def normalize(%Activity{object: %Object{} = object}, _), do: object
+  def normalize(%Object{} = object, _, _), do: object
+  def normalize(%Activity{object: %Object{} = object}, _, _), do: object
 
   # A hack for fake activities
-  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
+  def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
     %Object{id: "pleroma:fake_object_id", data: data}
   end
 
   # Catch and log Object.normalize() calls where the Activity's child object is not
   # preloaded.
-  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote) do
+  def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
     Logger.debug(
       "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!"
     )
@@ -67,7 +67,7 @@ defmodule Pleroma.Object do
     normalize(ap_id, fetch_remote)
   end
 
-  def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote) do
+  def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
     Logger.debug(
       "Object.normalize() called without preloaded object (#{ap_id}).  Consider preloading the object!"
     )
@@ -78,10 +78,14 @@ defmodule Pleroma.Object do
   end
 
   # Old way, try fetching the object through cache.
-  def normalize(%{"id" => ap_id}, fetch_remote), do: normalize(ap_id, fetch_remote)
-  def normalize(ap_id, false) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
-  def normalize(ap_id, true) when is_binary(ap_id), do: Fetcher.fetch_object_from_id!(ap_id)
-  def normalize(_, _), do: nil
+  def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
+  def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
+
+  def normalize(ap_id, true, options) when is_binary(ap_id) do
+    Fetcher.fetch_object_from_id!(ap_id, options)
+  end
+
+  def normalize(_, _, _), do: nil
 
   # Owned objects can only be mutated by their owner
   def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
index c422490ac4ae922399589d7ee842e5eb21a4a434..fffbf2bbb23062bdb983e922d95bf0e6f1aed069 100644 (file)
@@ -22,7 +22,7 @@ defmodule Pleroma.Object.Fetcher do
 
   # TODO:
   # This will create a Create activity, which we need internally at the moment.
-  def fetch_object_from_id(id) do
+  def fetch_object_from_id(id, options \\ []) do
     if object = Object.get_cached_by_ap_id(id) do
       {:ok, object}
     else
@@ -38,7 +38,7 @@ defmodule Pleroma.Object.Fetcher do
              "object" => data
            },
            :ok <- Containment.contain_origin(id, params),
-           {:ok, activity} <- Transmogrifier.handle_incoming(params),
+           {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
            {:object, _data, %Object{} = object} <-
              {:object, data, Object.normalize(activity, false)} do
         {:ok, object}
@@ -63,8 +63,8 @@ defmodule Pleroma.Object.Fetcher do
     end
   end
 
-  def fetch_object_from_id!(id) do
-    with {:ok, object} <- fetch_object_from_id(id) do
+  def fetch_object_from_id!(id, options \\ []) do
+    with {:ok, object} <- fetch_object_from_id(id, options) do
       object
     else
       _e ->
diff --git a/lib/pleroma/reverse_proxy/client.ex b/lib/pleroma/reverse_proxy/client.ex
new file mode 100644 (file)
index 0000000..57c2d2c
--- /dev/null
@@ -0,0 +1,24 @@
+defmodule Pleroma.ReverseProxy.Client do
+  @callback request(atom(), String.t(), [tuple()], String.t(), list()) ::
+              {:ok, pos_integer(), [tuple()], reference() | map()}
+              | {:ok, pos_integer(), [tuple()]}
+              | {:ok, reference()}
+              | {:error, term()}
+
+  @callback stream_body(reference() | pid() | map()) ::
+              {:ok, binary()} | :done | {:error, String.t()}
+
+  @callback close(reference() | pid() | map()) :: :ok
+
+  def request(method, url, headers, "", opts \\ []) do
+    client().request(method, url, headers, "", opts)
+  end
+
+  def stream_body(ref), do: client().stream_body(ref)
+
+  def close(ref), do: client().close(ref)
+
+  defp client do
+    Pleroma.Config.get([Pleroma.ReverseProxy.Client], :hackney)
+  end
+end
similarity index 97%
rename from lib/pleroma/reverse_proxy.ex
rename to lib/pleroma/reverse_proxy/reverse_proxy.ex
index de0f6e1bc6ed73caec4b481bd81565745f89c367..bf31e9cba26a1b2919732b75f3237fe444b9a622 100644 (file)
@@ -146,7 +146,7 @@ defmodule Pleroma.ReverseProxy do
     Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
     method = method |> String.downcase() |> String.to_existing_atom()
 
-    case hackney().request(method, url, headers, "", hackney_opts) do
+    case client().request(method, url, headers, "", hackney_opts) do
       {:ok, code, headers, client} when code in @valid_resp_codes ->
         {:ok, code, downcase_headers(headers), client}
 
@@ -173,7 +173,7 @@ defmodule Pleroma.ReverseProxy do
         halt(conn)
 
       {:error, :closed, conn} ->
-        :hackney.close(client)
+        client().close(client)
         halt(conn)
 
       {:error, error, conn} ->
@@ -181,7 +181,7 @@ defmodule Pleroma.ReverseProxy do
           "#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
         )
 
-        :hackney.close(client)
+        client().close(client)
         halt(conn)
     end
   end
@@ -196,7 +196,7 @@ defmodule Pleroma.ReverseProxy do
              duration,
              Keyword.get(opts, :max_read_duration, @max_read_duration)
            ),
-         {:ok, data} <- hackney().stream_body(client),
+         {:ok, data} <- client().stream_body(client),
          {:ok, duration} <- increase_read_duration(duration),
          sent_so_far = sent_so_far + byte_size(data),
          :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
@@ -378,5 +378,5 @@ defmodule Pleroma.ReverseProxy do
     {:ok, :no_duration_limit, :no_duration_limit}
   end
 
-  defp hackney, do: Pleroma.Config.get(:hackney, :hackney)
+  defp client, do: Pleroma.ReverseProxy.Client
 end
index f7191762fa97b7a3a2e3183377a44550cbc9b1ad..09f86aaa2d0a840ddaf57b757249356b1204ef14 100644 (file)
@@ -836,15 +836,12 @@ defmodule Pleroma.User do
   def mutes?(nil, _), do: false
   def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
 
-  def blocks?(user, %{ap_id: ap_id}) do
-    blocks = user.info.blocks
-    domain_blocks = user.info.domain_blocks
+  def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
+    blocks = info.blocks
+    domain_blocks = info.domain_blocks
     %{host: host} = URI.parse(ap_id)
 
-    Enum.member?(blocks, ap_id) ||
-      Enum.any?(domain_blocks, fn domain ->
-        host == domain
-      end)
+    Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
   end
 
   def subscribed_to?(user, %{ap_id: ap_id}) do
index ed06c2ab92f2f367a90c7176279b12bb408b8b42..64eb6d2bc484df842a22562b7468d80d807aba86 100644 (file)
@@ -43,6 +43,8 @@ defmodule Pleroma.User.Search do
   defp search_query(query_string, for_user, following) do
     for_user
     |> base_query(following)
+    |> filter_blocked_user(for_user)
+    |> filter_blocked_domains(for_user)
     |> search_subqueries(query_string)
     |> union_subqueries
     |> distinct_query()
@@ -55,6 +57,25 @@ defmodule Pleroma.User.Search do
   defp base_query(_user, false), do: User
   defp base_query(user, true), do: User.get_followers_query(user)
 
+  defp filter_blocked_user(query, %User{info: %{blocks: blocks}})
+       when length(blocks) > 0 do
+    from(q in query, where: not (q.ap_id in ^blocks))
+  end
+
+  defp filter_blocked_user(query, _), do: query
+
+  defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}})
+       when length(domain_blocks) > 0 do
+    domains = Enum.join(domain_blocks, ",")
+
+    from(
+      q in query,
+      where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
+    )
+  end
+
+  defp filter_blocked_domains(query, _), do: query
+
   defp paginate(query, limit, offset) do
     from(q in query, limit: ^limit, offset: ^offset)
   end
@@ -129,7 +150,7 @@ defmodule Pleroma.User.Search do
   @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
   defp fts_search_subquery(query, term) do
     processed_query =
-      term
+      String.trim_trailing(term, "@" <> local_domain())
       |> String.replace(~r/\W+/, " ")
       |> String.trim()
       |> String.split()
@@ -171,6 +192,8 @@ defmodule Pleroma.User.Search do
 
   @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
   defp trigram_search_subquery(query, term) do
+    term = String.trim_trailing(term, "@" <> local_domain())
+
     from(
       u in query,
       select_merge: %{
@@ -188,4 +211,6 @@ defmodule Pleroma.User.Search do
     )
     |> User.restrict_deactivated()
   end
+
+  defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
 end
index 3bb8b40b5c0284024ba2558950e1fee312ac965b..543d4bb7d1dd89c7e03ec0df027bb4a78ba769fd 100644 (file)
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
+  alias Pleroma.Web.Federator
 
   import Ecto.Query
 
@@ -22,20 +23,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   @doc """
   Modifies an incoming AP object (mastodon format) to our internal format.
   """
-  def fix_object(object) do
+  def fix_object(object, options \\ []) do
     object
     |> fix_actor
     |> fix_url
     |> fix_attachments
     |> fix_context
-    |> fix_in_reply_to
+    |> fix_in_reply_to(options)
     |> fix_emoji
     |> fix_tag
     |> fix_content_map
     |> fix_likes
     |> fix_addressing
     |> fix_summary
-    |> fix_type
+    |> fix_type(options)
   end
 
   def fix_summary(%{"summary" => nil} = object) do
@@ -164,7 +165,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     object
   end
 
-  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
+  def fix_in_reply_to(object, options \\ [])
+
+  def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
       when not is_nil(in_reply_to) do
     in_reply_to_id =
       cond do
@@ -182,28 +185,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
           ""
       end
 
-    case get_obj_helper(in_reply_to_id) do
-      {:ok, replied_object} ->
-        with %Activity{} = _activity <-
-               Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
-          object
-          |> Map.put("inReplyTo", replied_object.data["id"])
-          |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
-          |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
-          |> Map.put("context", replied_object.data["context"] || object["conversation"])
-        else
-          e ->
-            Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
+    object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
+
+    if Federator.allowed_incoming_reply_depth?(options[:depth]) do
+      case get_obj_helper(in_reply_to_id, options) do
+        {:ok, replied_object} ->
+          with %Activity{} = _activity <-
+                 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
             object
-        end
+            |> Map.put("inReplyTo", replied_object.data["id"])
+            |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
+            |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
+            |> Map.put("context", replied_object.data["context"] || object["conversation"])
+          else
+            e ->
+              Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
+              object
+          end
 
-      e ->
-        Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
-        object
+        e ->
+          Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
+          object
+      end
+    else
+      object
     end
   end
 
-  def fix_in_reply_to(object), do: object
+  def fix_in_reply_to(object, _options), do: object
 
   def fix_context(object) do
     context = object["context"] || object["conversation"] || Utils.generate_context_id()
@@ -336,8 +345,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
   def fix_content_map(object), do: object
 
-  def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do
-    reply = Object.normalize(reply_id)
+  def fix_type(object, options \\ [])
+
+  def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do
+    reply =
+      if Federator.allowed_incoming_reply_depth?(options[:depth]) do
+        Object.normalize(reply_id, true)
+      end
 
     if reply && (reply.data["type"] == "Question" and object["name"]) do
       Map.put(object, "type", "Answer")
@@ -346,7 +360,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end
   end
 
-  def fix_type(object), do: object
+  def fix_type(object, _), do: object
 
   defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
     with true <- id =~ "follows",
@@ -374,9 +388,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end
   end
 
+  def handle_incoming(data, options \\ [])
+
   # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
   # with nil ID.
-  def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
+  def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
     with context <- data["context"] || Utils.generate_context_id(),
          content <- data["content"] || "",
          %User{} = actor <- User.get_cached_by_ap_id(actor),
@@ -409,15 +425,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   # disallow objects with bogus IDs
-  def handle_incoming(%{"id" => nil}), do: :error
-  def handle_incoming(%{"id" => ""}), do: :error
+  def handle_incoming(%{"id" => nil}, _options), do: :error
+  def handle_incoming(%{"id" => ""}, _options), do: :error
   # length of https:// = 8, should validate better, but good enough for now.
-  def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
+  def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
+    do: :error
 
   # TODO: validate those with a Ecto scheme
   # - tags
   # - emoji
-  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
+  def handle_incoming(
+        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
+        options
+      )
       when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
     actor = Containment.get_actor(data)
 
@@ -427,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
     with nil <- Activity.get_create_by_object_ap_id(object["id"]),
          {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
-      object = fix_object(data["object"])
+      options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
+      object = fix_object(data["object"], options)
 
       params = %{
         to: data["to"],
@@ -452,7 +473,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def handle_incoming(
-        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
+        %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
+        _options
       ) do
     with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
          {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
@@ -503,7 +525,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def handle_incoming(
-        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
+        %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
+        _options
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
@@ -524,7 +547,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def handle_incoming(
-        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
+        %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
+        _options
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
@@ -548,7 +572,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def handle_incoming(
-        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
+        %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
+        _options
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@@ -561,7 +586,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def handle_incoming(
-        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
+        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
+        _options
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@@ -576,7 +602,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
   def handle_incoming(
         %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
-          data
+          data,
+        _options
       )
       when object_type in ["Person", "Application", "Service", "Organization"] do
     with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
@@ -614,7 +641,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   # an error or a tombstone.  This would allow us to verify that a deletion actually took
   # place.
   def handle_incoming(
-        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
+        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data,
+        _options
       ) do
     object_id = Utils.get_ap_id(object_id)
 
@@ -635,7 +663,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
           "object" => %{"type" => "Announce", "object" => object_id},
           "actor" => _actor,
           "id" => id
-        } = data
+        } = data,
+        _options
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@@ -653,7 +682,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
           "object" => %{"type" => "Follow", "object" => followed},
           "actor" => follower,
           "id" => id
-        } = _data
+        } = _data,
+        _options
       ) do
     with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
          {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
@@ -671,7 +701,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
           "object" => %{"type" => "Block", "object" => blocked},
           "actor" => blocker,
           "id" => id
-        } = _data
+        } = _data,
+        _options
       ) do
     with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
          %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
@@ -685,7 +716,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def handle_incoming(
-        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
+        %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
+        _options
       ) do
     with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
          %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
@@ -705,7 +737,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
           "object" => %{"type" => "Like", "object" => object_id},
           "actor" => _actor,
           "id" => id
-        } = data
+        } = data,
+        _options
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
@@ -717,10 +750,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end
   end
 
-  def handle_incoming(_), do: :error
+  def handle_incoming(_, _), do: :error
 
-  def get_obj_helper(id) do
-    if object = Object.normalize(id), do: {:ok, object}, else: nil
+  def get_obj_helper(id, options \\ []) do
+    if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
   end
 
   def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
index 514266ceefaa314484dbf9d840a51a1525195725..4288ea4c838a8cf291394663291246bea3d7c9bd 100644 (file)
@@ -170,14 +170,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do
   Enqueues an activity for federation if it's local
   """
   def maybe_federate(%Activity{local: true} = activity) do
-    priority =
-      case activity.data["type"] do
-        "Delete" -> 10
-        "Create" -> 1
-        _ -> 5
-      end
+    if Pleroma.Config.get!([:instance, :federating]) do
+      priority =
+        case activity.data["type"] do
+          "Delete" -> 10
+          "Create" -> 1
+          _ -> 5
+        end
+
+      Pleroma.Web.Federator.publish(activity, priority)
+    end
 
-    Pleroma.Web.Federator.publish(activity, priority)
     :ok
   end
 
index f4c9fe28403d4991f6569b7bc91897529fbcd155..f4f9e83e06ab55f71b715301631865bc46b11c00 100644 (file)
@@ -22,6 +22,18 @@ defmodule Pleroma.Web.Federator do
     refresh_subscriptions()
   end
 
+  @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
+  # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
+  def allowed_incoming_reply_depth?(depth) do
+    max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
+
+    if max_replies_depth do
+      (depth || 1) <= max_replies_depth
+    else
+      true
+    end
+  end
+
   # Client API
 
   def incoming_doc(doc) do
index 0d1e2355d203d73350fea6adbf5b8036d9956df9..efa9cc78824eb00add3735568e57a89943b5d69e 100644 (file)
@@ -17,8 +17,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
 
   def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-    accounts = User.search(query, search_options(params, user))
-    statuses = Activity.search(user, query)
+    accounts = with_fallback(fn -> User.search(query, search_options(params, user)) end, [])
+    statuses = with_fallback(fn -> Activity.search(user, query) end, [])
     tags_path = Web.base_url() <> "/tag/"
 
     tags =
@@ -40,8 +40,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
   end
 
   def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
-    accounts = User.search(query, search_options(params, user))
-    statuses = Activity.search(user, query)
+    accounts = with_fallback(fn -> User.search(query, search_options(params, user)) end, [])
+    statuses = with_fallback(fn -> Activity.search(user, query) end, [])
 
     tags =
       query
@@ -76,4 +76,14 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
       for_user: user
     ]
   end
+
+  defp with_fallback(f, fallback) do
+    try do
+      f.()
+    rescue
+      error ->
+        Logger.error("#{__MODULE__} search error: #{inspect(error)}")
+        fallback
+    end
+  end
 end
index 6836d331a7ae89c5d2e58102f68ae55a26c5f698..ec582b919c626aa5c8f1e9330a03c0d1afafac8d 100644 (file)
@@ -104,7 +104,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       id: to_string(activity.id),
       uri: activity_object.data["id"],
       url: activity_object.data["id"],
-      account: AccountView.render("account.json", %{user: user}),
+      account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
       in_reply_to_id: nil,
       in_reply_to_account_id: nil,
       reblog: reblogged,
@@ -221,7 +221,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       id: to_string(activity.id),
       uri: object.data["id"],
       url: url,
-      account: AccountView.render("account.json", %{user: user}),
+      account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
       in_reply_to_id: reply_to && to_string(reply_to.id),
       in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
       reblog: nil,
index 3299e1721948a754c3fba44dd4d4863aa5be3ae3..dbd3542eadcc57929addcbbad7f682fa95d34d2d 100644 (file)
@@ -29,9 +29,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
 
   def init(%{qs: qs} = req, state) do
     with params <- :cow_qs.parse_qs(qs),
+         sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
          access_token <- List.keyfind(params, "access_token", 0),
          {_, stream} <- List.keyfind(params, "stream", 0),
-         {:ok, user} <- allow_request(stream, access_token),
+         {:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
          topic when is_binary(topic) <- expand_topic(stream, params) do
       {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
     else
@@ -84,13 +85,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
   end
 
   # Public streams without authentication.
-  defp allow_request(stream, nil) when stream in @anonymous_streams do
+  defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do
     {:ok, nil}
   end
 
   # Authenticated streams.
-  defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
-    with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
+  defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do
+    token =
+      with {"access_token", token} <- access_token do
+        token
+      else
+        _ -> sec_websocket
+      end
+
+    with true <- is_bitstring(token),
+         %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
          user = %User{} <- User.get_cached_by_id(user_id) do
       {:ok, user}
     else
index cee6d8481a33942d1d41d51a0cb16ba6fb98ac0b..dd8888a021635357b4fe88c1bb2748517c7c6062 100644 (file)
@@ -33,20 +33,7 @@ defmodule Pleroma.Web.MediaProxy do
 
   def encode_url(url) do
     secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
-
-    # Must preserve `%2F` for compatibility with S3
-    # https://git.pleroma.social/pleroma/pleroma/issues/580
-    replacement = get_replacement(url, ":2F:")
-
-    # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
-    base64 =
-      url
-      |> String.replace("%2F", replacement)
-      |> URI.decode()
-      |> URI.encode()
-      |> String.replace(replacement, "%2F")
-      |> Base.url_encode64(@base64_opts)
-
+    base64 = Base.url_encode64(url, @base64_opts)
     sig = :crypto.hmac(:sha, secret, base64)
     sig64 = sig |> Base.url_encode64(@base64_opts)
 
@@ -80,12 +67,4 @@ defmodule Pleroma.Web.MediaProxy do
     |> Enum.filter(fn value -> value end)
     |> Path.join()
   end
-
-  defp get_replacement(url, replacement) do
-    if String.contains?(url, replacement) do
-      get_replacement(url, replacement <> replacement)
-    else
-      replacement
-    end
-  end
 end
index 357b80a2d50e7959bd8d5b8dfe36d2468f25def9..4033ec38ff17d2592777565c47c8dc9c62686cb8 100644 (file)
@@ -121,4 +121,6 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
       acc ++ rendered_tags
     end)
   end
+
+  defp build_attachments(_), do: []
 end
index 040b872e7034d840705ffb324628d223a9f197b3..8dd01e0d5f33ff55391dc4cd77ac1c52ff2b157e 100644 (file)
@@ -117,6 +117,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
     end)
   end
 
+  defp build_attachments(_id, _object), do: []
+
   defp player_url(id) do
     Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id)
   end
index 32be430b7691aa31ab695893456362a3d953cafe..869dda5c5cd3d44eac41aa853ff23d92b1332e8e 100644 (file)
@@ -162,7 +162,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
         accountActivationRequired: Config.get([:instance, :account_activation_required], false),
         invitesEnabled: Config.get([:instance, :invites_enabled], false),
         features: features,
-        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames])
+        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
+        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)
       }
     }
   end
index ec6e5cfaf3226cdb61e4ba7b1e72d3bba161a532..8e0adad91f38b7c87008f37b932e426934eb0dd8 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.Federator
   alias Pleroma.Web.OStatus
   alias Pleroma.Web.XML
 
@@ -88,14 +89,15 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
     Map.put(note, "external_url", url)
   end
 
-  def fetch_replied_to_activity(entry, in_reply_to) do
+  def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do
     with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
       activity
     else
       _e ->
-        with in_reply_to_href when not is_nil(in_reply_to_href) <-
+        with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
+             in_reply_to_href when not is_nil(in_reply_to_href) <-
                XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
-             {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do
+             {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do
           activity
         else
           _e -> nil
@@ -104,7 +106,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
   end
 
   # TODO: Clean this up a bit.
-  def handle_note(entry, doc \\ nil) do
+  def handle_note(entry, doc \\ nil, options \\ []) do
     with id <- XML.string_from_xpath("//id", entry),
          activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
          [author] <- :xmerl_xpath.string('//author[1]', doc),
@@ -112,7 +114,8 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
          content_html <- OStatus.get_content(entry),
          cw <- OStatus.get_cw(entry),
          in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
-         in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
+         options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1),
+         in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options),
          in_reply_to_object <-
            (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
          in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
index 6ed089d8458f6e51aacf0ca3da25462559340faf..502410c83cfd7ed0bf7620970f10f1f996bfd021 100644 (file)
@@ -54,7 +54,7 @@ defmodule Pleroma.Web.OStatus do
     "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
   end
 
-  def handle_incoming(xml_string) do
+  def handle_incoming(xml_string, options \\ []) do
     with doc when doc != :error <- parse_document(xml_string) do
       with {:ok, actor_user} <- find_make_or_update_user(doc),
            do: Pleroma.Instances.set_reachable(actor_user.ap_id)
@@ -91,10 +91,12 @@ defmodule Pleroma.Web.OStatus do
               _ ->
                 case object_type do
                   'http://activitystrea.ms/schema/1.0/note' ->
-                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
+                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
+                         do: activity
 
                   'http://activitystrea.ms/schema/1.0/comment' ->
-                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
+                    with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
+                         do: activity
 
                   _ ->
                     Logger.error("Couldn't parse incoming document")
@@ -359,7 +361,7 @@ defmodule Pleroma.Web.OStatus do
     end
   end
 
-  def fetch_activity_from_atom_url(url) do
+  def fetch_activity_from_atom_url(url, options \\ []) do
     with true <- String.starts_with?(url, "http"),
          {:ok, %{body: body, status: code}} when code in 200..299 <-
            HTTP.get(
@@ -367,7 +369,7 @@ defmodule Pleroma.Web.OStatus do
              [{:Accept, "application/atom+xml"}]
            ) do
       Logger.debug("Got document from #{url}, handling...")
-      handle_incoming(body)
+      handle_incoming(body, options)
     else
       e ->
         Logger.debug("Couldn't get #{url}: #{inspect(e)}")
@@ -375,13 +377,13 @@ defmodule Pleroma.Web.OStatus do
     end
   end
 
-  def fetch_activity_from_html_url(url) do
+  def fetch_activity_from_html_url(url, options \\ []) do
     Logger.debug("Trying to fetch #{url}")
 
     with true <- String.starts_with?(url, "http"),
          {:ok, %{body: body}} <- HTTP.get(url, []),
          {:ok, atom_url} <- get_atom_url(body) do
-      fetch_activity_from_atom_url(atom_url)
+      fetch_activity_from_atom_url(atom_url, options)
     else
       e ->
         Logger.debug("Couldn't get #{url}: #{inspect(e)}")
@@ -389,11 +391,11 @@ defmodule Pleroma.Web.OStatus do
     end
   end
 
-  def fetch_activity_from_url(url) do
-    with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
+  def fetch_activity_from_url(url, options \\ []) do
+    with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
       {:ok, activities}
     else
-      _e -> fetch_activity_from_html_url(url)
+      _e -> fetch_activity_from_html_url(url, options)
     end
   rescue
     e ->
index 055289dc56bfbfb31d336f9096fb7c06877d72b8..ff9ed1640c069b9a032560be9124e5a291eac439 100644 (file)
@@ -724,6 +724,7 @@ end
 
 defmodule Fallback.RedirectController do
   use Pleroma.Web, :controller
+  require Logger
   alias Pleroma.User
   alias Pleroma.Web.Metadata
 
@@ -750,7 +751,20 @@ defmodule Fallback.RedirectController do
 
   def redirector_with_meta(conn, params) do
     {:ok, index_content} = File.read(index_file_path())
-    tags = Metadata.build_tags(params)
+
+    tags =
+      try do
+        Metadata.build_tags(params)
+      rescue
+        e ->
+          Logger.error(
+            "Metadata rendering for #{conn.request_path} failed.\n" <>
+              Exception.format(:error, e, __STACKTRACE__)
+          )
+
+          ""
+      end
+
     response = String.replace(index_content, "<!--server-generated-meta-->", tags)
 
     conn
diff --git a/mix.exs b/mix.exs
index 518de590dfb8df999929aaff545713ce74360549..8f64562ef947700b1d2407efc5845aa10dd458c7 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -150,7 +150,8 @@ defmodule Pleroma.Mixfile do
       {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
       {:ex_rated, "~> 1.3"},
       {:plug_static_index_html, "~> 1.0.0"},
-      {:excoveralls, "~> 0.11.1", only: :test}
+      {:excoveralls, "~> 0.11.1", only: :test},
+      {:mox, "~> 0.5", only: :test}
     ] ++ oauth_deps()
   end
 
@@ -173,10 +174,14 @@ defmodule Pleroma.Mixfile do
   # Builds a version string made of:
   # * the application version
   # * a pre-release if ahead of the tag: the describe string (-count-commithash)
-  # * build info:
+  # * branch name
+  # * build metadata:
   #   * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
   #   * the mix environment if different than prod
   defp version(version) do
+    identifier_filter = ~r/[^0-9a-z\-]+/i
+
+    # Pre-release version, denoted from patch version with a hyphen
     {git_tag, git_pre_release} =
       with {tag, 0} <-
              System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
@@ -197,6 +202,19 @@ defmodule Pleroma.Mixfile do
       )
     end
 
+    # Branch name as pre-release version component, denoted with a dot
+    branch_name =
+      with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
+           branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
+           true <- branch_name != "master" do
+        branch_name =
+          branch_name
+          |> String.trim()
+          |> String.replace(identifier_filter, "-")
+
+        "." <> branch_name
+      end
+
     build_name =
       cond do
         name = Application.get_env(:pleroma, :build_name) -> name
@@ -205,37 +223,26 @@ defmodule Pleroma.Mixfile do
       end
 
     env_name = if Mix.env() != :prod, do: to_string(Mix.env())
-
     env_override = System.get_env("PLEROMA_BUILD_ENV")
 
     env_name =
-      if env_override do
-        if env_override != "prod", do: env_override
-      else
-        env_name
+      case env_override do
+        nil -> env_name
+        env_override when env_override in ["", "prod"] -> nil
+        env_override -> env_override
       end
 
-    build =
+    # Build metadata, denoted with a plus sign
+    build_metadata =
       [build_name, env_name]
       |> Enum.filter(fn string -> string && string != "" end)
-      |> Enum.join("-")
+      |> Enum.join(".")
       |> (fn
             "" -> nil
-            string -> "+" <> string
+            string -> "+" <> String.replace(string, identifier_filter, "-")
           end).()
 
-    branch_name =
-      with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
-           branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
-           true <- branch_name != "master" do
-        branch_name =
-          String.trim(branch_name)
-          |> String.replace(~r/[^0-9a-z\-\.]+/i, "-")
-
-        "-" <> branch_name
-      end
-
-    [version, git_pre_release, branch_name, build]
+    [version, git_pre_release, branch_name, build_metadata]
     |> Enum.filter(fn string -> string && string != "" end)
     |> Enum.join()
   end
index cae8d7d8459713857e8e6df57b865d3df31cfa92..e711be635b801af7690f4ba9b4a181bae9aee6ca 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -52,6 +52,7 @@
   "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
   "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
   "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
+  "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"},
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
   "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
   "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
   "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
-  "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
   "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
   "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"},
   "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
   "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
   "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
   "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"},
-  "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
   "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"},
   "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
   "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
index c7565946e022b342a6f794789d3517e396529f05..9a67727e9965830d39e9c2c5bc00b7aacbac855e 100644 (file)
@@ -1,19 +1,31 @@
 defmodule Pleroma.Repo.Migrations.CaseInsensivtivity do
   use Ecto.Migration
 
+  # Two-steps alters are intentional.
+  # When alter of 2 columns is done in a single operation,
+  # inconsistent failures happen because of index on `email` column.
+
   def up do
-    execute ("create extension if not exists citext")
+    execute("create extension if not exists citext")
+
+    alter table(:users) do
+      modify(:email, :citext)
+    end
+
     alter table(:users) do
-      modify :email, :citext
-      modify :nickname, :citext
+      modify(:nickname, :citext)
     end
   end
 
   def down do
     alter table(:users) do
-      modify :email, :string
-      modify :nickname, :string
+      modify(:email, :string)
     end
-    execute ("drop extension if exists citext")
+
+    alter table(:users) do
+      modify(:nickname, :string)
+    end
+
+    execute("drop extension if exists citext")
   end
 end
index 9c67b209b95ccff744455e92055e7a7974735304..e731d20eb7e6ba27321d70d365870b96d930c4e1 100755 (executable)
@@ -30,12 +30,15 @@ detect_flavour() {
 
 detect_branch() {
        version="$(cut -d' ' -f2 <"$RELEASE_ROOT"/releases/start_erl.data)"
-       branch="$(echo "$version" | cut -d'-' -f 4)"
+       # Expected format: major.minor.patch_version(-number_of_commits_ahead_of_tag-gcommit_hash).branch
+       branch="$(echo "$version" | cut -d'.' -f 4)"
        if [ "$branch" = "develop" ]; then
                echo "develop"
        elif [ "$branch" = "" ]; then
                echo "master"
        else
+         # Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols —
+         #   if supporting releases for more branches, need to ensure they contain only these symbols.
                echo "Releases are built only for master and develop branches" >&2
                exit 1
        fi
index 5903d10ff05b1ffd94b4d4594952a1b393283b6f..aa193e0d40dadf907f974ef2cfa378ee7788844e 100644 (file)
@@ -11,6 +11,16 @@ defmodule Pleroma.ConversationTest do
 
   import Pleroma.Factory
 
+  setup_all do
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    Pleroma.Config.put(config_path, true)
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+
+    :ok
+  end
+
   test "it goes through old direct conversations" do
     user = insert(:user)
     other_user = insert(:user)
diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs
new file mode 100644 (file)
index 0000000..a368999
--- /dev/null
@@ -0,0 +1,91 @@
+defmodule Pleroma.HTTP.RequestBuilderTest do
+  use ExUnit.Case, async: true
+  alias Pleroma.HTTP.RequestBuilder
+
+  describe "headers/2" do
+    test "don't send pleroma user agent" do
+      assert RequestBuilder.headers(%{}, []) == %{headers: []}
+    end
+
+    test "send pleroma user agent" do
+      send = Pleroma.Config.get([:http, :send_user_agent])
+      Pleroma.Config.put([:http, :send_user_agent], true)
+
+      on_exit(fn ->
+        Pleroma.Config.put([:http, :send_user_agent], send)
+      end)
+
+      assert RequestBuilder.headers(%{}, []) == %{
+               headers: [{"User-Agent", Pleroma.Application.user_agent()}]
+             }
+    end
+  end
+
+  describe "add_optional_params/3" do
+    test "don't add if keyword is empty" do
+      assert RequestBuilder.add_optional_params(%{}, %{}, []) == %{}
+    end
+
+    test "add query parameter" do
+      assert RequestBuilder.add_optional_params(
+               %{},
+               %{query: :query, body: :body, another: :val},
+               [
+                 {:query, "param1=val1&param2=val2"},
+                 {:body, "some body"}
+               ]
+             ) == %{query: "param1=val1&param2=val2", body: "some body"}
+    end
+  end
+
+  describe "add_param/4" do
+    test "add file parameter" do
+      %{
+        body: %Tesla.Multipart{
+          boundary: _,
+          content_type_params: [],
+          parts: [
+            %Tesla.Multipart.Part{
+              body: %File.Stream{
+                line_or_bytes: 2048,
+                modes: [:raw, :read_ahead, :read, :binary],
+                path: "some-path/filename.png",
+                raw: true
+              },
+              dispositions: [name: "filename.png", filename: "filename.png"],
+              headers: []
+            }
+          ]
+        }
+      } = RequestBuilder.add_param(%{}, :file, "filename.png", "some-path/filename.png")
+    end
+
+    test "add key to body" do
+      %{
+        body: %Tesla.Multipart{
+          boundary: _,
+          content_type_params: [],
+          parts: [
+            %Tesla.Multipart.Part{
+              body: "\"someval\"",
+              dispositions: [name: "somekey"],
+              headers: ["Content-Type": "application/json"]
+            }
+          ]
+        }
+      } = RequestBuilder.add_param(%{}, :body, "somekey", "someval")
+    end
+
+    test "add form parameter" do
+      assert RequestBuilder.add_param(%{}, :form, "somename", "someval") == %{
+               body: %{"somename" => "someval"}
+             }
+    end
+
+    test "add for location" do
+      assert RequestBuilder.add_param(%{}, :some_location, "somekey", "someval") == %{
+               some_location: [{"somekey", "someval"}]
+             }
+    end
+  end
+end
index a604713d800ec83407049116282c78a49ad733b3..3975cdcd694b3a39bc14ebce532b89faaa021b55 100644 (file)
@@ -107,5 +107,12 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
       assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
       assert {:error, {403, "Forbidden"}} = start_socket("?stream=user:notification")
     end
+
+    test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
+      assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
+
+      assert {:error, {403, "Forbidden"}} =
+               start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
+    end
   end
 end
index b23aeb88be580c9642dd6675a55ded62d1af951a..1d6d170b72e02d4986ad087fb3a090c7cb59a391 100644 (file)
@@ -70,14 +70,6 @@ defmodule Pleroma.MediaProxyTest do
       assert decode_result(encoded) == url
     end
 
-    test "ensures urls are url-encoded" do
-      assert decode_result(url("https://pleroma.social/Hello world.jpg")) ==
-               "https://pleroma.social/Hello%20world.jpg"
-
-      assert decode_result(url("https://pleroma.social/Hello%20world.jpg")) ==
-               "https://pleroma.social/Hello%20world.jpg"
-    end
-
     test "validates signature" do
       secret_key_base = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
 
@@ -141,10 +133,31 @@ defmodule Pleroma.MediaProxyTest do
       assert String.starts_with?(encoded, Pleroma.Config.get([:media_proxy, :base_url]))
     end
 
-    # https://git.pleroma.social/pleroma/pleroma/issues/580
-    test "encoding S3 links (must preserve `%2F`)" do
+    # Some sites expect ASCII encoded characters in the URL to be preserved even if
+    # unnecessary.
+    # Issues: https://git.pleroma.social/pleroma/pleroma/issues/580
+    #         https://git.pleroma.social/pleroma/pleroma/issues/1055
+    test "preserve ASCII encoding" do
+      url =
+        "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF"
+
+      encoded = url(url)
+      assert decode_result(encoded) == url
+    end
+
+    # This includes unsafe/reserved characters which are not interpreted as part of the URL
+    # and would otherwise have to be ASCII encoded. It is our role to ensure the proxied URL
+    # is unmodified, so we are testing these characters anyway.
+    test "preserve non-unicode characters per RFC3986" do
       url =
-        "https://s3.amazonaws.com/example/test.png?X-Amz-Credential=your-access-key-id%2F20130721%2Fus-east-1%2Fs3%2Faws4_request"
+        "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}"
+
+      encoded = url(url)
+      assert decode_result(encoded) == url
+    end
+
+    test "preserve unicode characters" do
+      url = "https://ko.wikipedia.org/wiki/위키백과:대문"
 
       encoded = url(url)
       assert decode_result(encoded) == url
diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy_test.exs
new file mode 100644 (file)
index 0000000..75a6144
--- /dev/null
@@ -0,0 +1,297 @@
+defmodule Pleroma.ReverseProxyTest do
+  use Pleroma.Web.ConnCase, async: true
+  import ExUnit.CaptureLog
+  import ExUnit.CaptureLog
+  import Mox
+  alias Pleroma.ReverseProxy
+  alias Pleroma.ReverseProxy.ClientMock
+
+  setup_all do
+    {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.ReverseProxy.ClientMock)
+    :ok
+  end
+
+  setup :verify_on_exit!
+
+  defp user_agent_mock(user_agent, invokes) do
+    json = Jason.encode!(%{"user-agent": user_agent})
+
+    ClientMock
+    |> expect(:request, fn :get, url, _, _, _ ->
+      Registry.register(Pleroma.ReverseProxy.ClientMock, url, 0)
+
+      {:ok, 200,
+       [
+         {"content-type", "application/json"},
+         {"content-length", byte_size(json) |> to_string()}
+       ], %{url: url}}
+    end)
+    |> expect(:stream_body, invokes, fn %{url: url} ->
+      case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
+        [{_, 0}] ->
+          Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
+          {:ok, json}
+
+        [{_, 1}] ->
+          Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
+          :done
+      end
+    end)
+  end
+
+  describe "user-agent" do
+    test "don't keep", %{conn: conn} do
+      user_agent_mock("hackney/1.15.1", 2)
+      conn = ReverseProxy.call(conn, "/user-agent")
+      assert json_response(conn, 200) == %{"user-agent" => "hackney/1.15.1"}
+    end
+
+    test "keep", %{conn: conn} do
+      user_agent_mock(Pleroma.Application.user_agent(), 2)
+      conn = ReverseProxy.call(conn, "/user-agent-keep", keep_user_agent: true)
+      assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
+    end
+  end
+
+  test "closed connection", %{conn: conn} do
+    ClientMock
+    |> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end)
+    |> expect(:stream_body, fn _ -> {:error, :closed} end)
+    |> expect(:close, fn _ -> :ok end)
+
+    conn = ReverseProxy.call(conn, "/closed")
+    assert conn.halted
+  end
+
+  describe "max_body " do
+    test "length returns error if content-length more than option", %{conn: conn} do
+      user_agent_mock("hackney/1.15.1", 0)
+
+      assert capture_log(fn ->
+               ReverseProxy.call(conn, "/user-agent", max_body_length: 4)
+             end) =~
+               "[error] Elixir.Pleroma.ReverseProxy: request to \"/user-agent\" failed: :body_too_large"
+    end
+
+    defp stream_mock(invokes, with_close? \\ false) do
+      ClientMock
+      |> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
+        Registry.register(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length, 0)
+
+        {:ok, 200, [{"content-type", "application/octet-stream"}],
+         %{url: "/stream-bytes/" <> length}}
+      end)
+      |> expect(:stream_body, invokes, fn %{url: "/stream-bytes/" <> length} ->
+        max = String.to_integer(length)
+
+        case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length) do
+          [{_, current}] when current < max ->
+            Registry.update_value(
+              Pleroma.ReverseProxy.ClientMock,
+              "/stream-bytes/" <> length,
+              &(&1 + 10)
+            )
+
+            {:ok, "0123456789"}
+
+          [{_, ^max}] ->
+            Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/stream-bytes/" <> length)
+            :done
+        end
+      end)
+
+      if with_close? do
+        expect(ClientMock, :close, fn _ -> :ok end)
+      end
+    end
+
+    test "max_body_size returns error if streaming body more than that option", %{conn: conn} do
+      stream_mock(3, true)
+
+      assert capture_log(fn ->
+               ReverseProxy.call(conn, "/stream-bytes/50", max_body_size: 30)
+             end) =~
+               "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
+    end
+  end
+
+  describe "HEAD requests" do
+    test "common", %{conn: conn} do
+      ClientMock
+      |> expect(:request, fn :head, "/head", _, _, _ ->
+        {:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
+      end)
+
+      conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
+      assert html_response(conn, 200) == ""
+    end
+  end
+
+  defp error_mock(status) when is_integer(status) do
+    ClientMock
+    |> expect(:request, fn :get, "/status/" <> _, _, _, _ ->
+      {:error, status}
+    end)
+  end
+
+  describe "returns error on" do
+    test "500", %{conn: conn} do
+      error_mock(500)
+
+      capture_log(fn -> ReverseProxy.call(conn, "/status/500") end) =~
+        "[error] Elixir.Pleroma.ReverseProxy: request to /status/500 failed with HTTP status 500"
+    end
+
+    test "400", %{conn: conn} do
+      error_mock(400)
+
+      capture_log(fn -> ReverseProxy.call(conn, "/status/400") end) =~
+        "[error] Elixir.Pleroma.ReverseProxy: request to /status/400 failed with HTTP status 400"
+    end
+
+    test "204", %{conn: conn} do
+      ClientMock
+      |> expect(:request, fn :get, "/status/204", _, _, _ -> {:ok, 204, [], %{}} end)
+
+      capture_log(fn ->
+        conn = ReverseProxy.call(conn, "/status/204")
+        assert conn.resp_body == "Request failed: No Content"
+        assert conn.halted
+      end) =~
+        "[error] Elixir.Pleroma.ReverseProxy: request to \"/status/204\" failed with HTTP status 204"
+    end
+  end
+
+  test "streaming", %{conn: conn} do
+    stream_mock(21)
+    conn = ReverseProxy.call(conn, "/stream-bytes/200")
+    assert conn.state == :chunked
+    assert byte_size(conn.resp_body) == 200
+    assert Plug.Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
+  end
+
+  defp headers_mock(_) do
+    ClientMock
+    |> expect(:request, fn :get, "/headers", headers, _, _ ->
+      Registry.register(Pleroma.ReverseProxy.ClientMock, "/headers", 0)
+      {:ok, 200, [{"content-type", "application/json"}], %{url: "/headers", headers: headers}}
+    end)
+    |> expect(:stream_body, 2, fn %{url: url, headers: headers} ->
+      case Registry.lookup(Pleroma.ReverseProxy.ClientMock, url) do
+        [{_, 0}] ->
+          Registry.update_value(Pleroma.ReverseProxy.ClientMock, url, &(&1 + 1))
+          headers = for {k, v} <- headers, into: %{}, do: {String.capitalize(k), v}
+          {:ok, Jason.encode!(%{headers: headers})}
+
+        [{_, 1}] ->
+          Registry.unregister(Pleroma.ReverseProxy.ClientMock, url)
+          :done
+      end
+    end)
+
+    :ok
+  end
+
+  describe "keep request headers" do
+    setup [:headers_mock]
+
+    test "header passes", %{conn: conn} do
+      conn =
+        Plug.Conn.put_req_header(
+          conn,
+          "accept",
+          "text/html"
+        )
+        |> ReverseProxy.call("/headers")
+
+      %{"headers" => headers} = json_response(conn, 200)
+      assert headers["Accept"] == "text/html"
+    end
+
+    test "header is filtered", %{conn: conn} do
+      conn =
+        Plug.Conn.put_req_header(
+          conn,
+          "accept-language",
+          "en-US"
+        )
+        |> ReverseProxy.call("/headers")
+
+      %{"headers" => headers} = json_response(conn, 200)
+      refute headers["Accept-Language"]
+    end
+  end
+
+  test "returns 400 on non GET, HEAD requests", %{conn: conn} do
+    conn = ReverseProxy.call(Map.put(conn, :method, "POST"), "/ip")
+    assert conn.status == 400
+  end
+
+  describe "cache resp headers" do
+    test "returns headers", %{conn: conn} do
+      ClientMock
+      |> expect(:request, fn :get, "/cache/" <> ttl, _, _, _ ->
+        {:ok, 200, [{"cache-control", "public, max-age=" <> ttl}], %{}}
+      end)
+      |> expect(:stream_body, fn _ -> :done end)
+
+      conn = ReverseProxy.call(conn, "/cache/10")
+      assert {"cache-control", "public, max-age=10"} in conn.resp_headers
+    end
+
+    test "add cache-control", %{conn: conn} do
+      ClientMock
+      |> expect(:request, fn :get, "/cache", _, _, _ ->
+        {:ok, 200, [{"ETag", "some ETag"}], %{}}
+      end)
+      |> expect(:stream_body, fn _ -> :done end)
+
+      conn = ReverseProxy.call(conn, "/cache")
+      assert {"cache-control", "public"} in conn.resp_headers
+    end
+  end
+
+  defp disposition_headers_mock(headers) do
+    ClientMock
+    |> expect(:request, fn :get, "/disposition", _, _, _ ->
+      Registry.register(Pleroma.ReverseProxy.ClientMock, "/disposition", 0)
+
+      {:ok, 200, headers, %{url: "/disposition"}}
+    end)
+    |> expect(:stream_body, 2, fn %{url: "/disposition"} ->
+      case Registry.lookup(Pleroma.ReverseProxy.ClientMock, "/disposition") do
+        [{_, 0}] ->
+          Registry.update_value(Pleroma.ReverseProxy.ClientMock, "/disposition", &(&1 + 1))
+          {:ok, ""}
+
+        [{_, 1}] ->
+          Registry.unregister(Pleroma.ReverseProxy.ClientMock, "/disposition")
+          :done
+      end
+    end)
+  end
+
+  describe "response content disposition header" do
+    test "not atachment", %{conn: conn} do
+      disposition_headers_mock([
+        {"content-type", "image/gif"},
+        {"content-length", 0}
+      ])
+
+      conn = ReverseProxy.call(conn, "/disposition")
+
+      assert {"content-type", "image/gif"} in conn.resp_headers
+    end
+
+    test "with content-disposition header", %{conn: conn} do
+      disposition_headers_mock([
+        {"content-disposition", "attachment; filename=\"filename.jpg\""},
+        {"content-length", 0}
+      ])
+
+      conn = ReverseProxy.call(conn, "/disposition")
+
+      assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
+    end
+  end
+end
index 6e389ce5254093200c1c414da9b570853b171b15..1a92be065b04501693d67c1200eda9e35b02b09c 100644 (file)
@@ -9,6 +9,12 @@ defmodule Pleroma.Tests.Helpers do
 
   defmacro __using__(_opts) do
     quote do
+      def collect_ids(collection) do
+        collection
+        |> Enum.map(& &1.id)
+        |> Enum.sort()
+      end
+
       def refresh_record(%{id: id, __struct__: model} = _),
         do: refresh_record(model, %{id: id})
 
diff --git a/test/tasks/ecto/ecto_test.exs b/test/tasks/ecto/ecto_test.exs
new file mode 100644 (file)
index 0000000..b48662c
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Mix.Tasks.Pleroma.EctoTest do
+  use ExUnit.Case, async: true
+
+  test "raise on bad path" do
+    assert_raise RuntimeError, ~r/Could not find migrations directory/, fn ->
+      Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo,
+        migrations_path: "some-path"
+      )
+    end
+  end
+end
diff --git a/test/tasks/pleroma_test.exs b/test/tasks/pleroma_test.exs
new file mode 100644 (file)
index 0000000..e236ccb
--- /dev/null
@@ -0,0 +1,46 @@
+defmodule Mix.PleromaTest do
+  use ExUnit.Case, async: true
+  import Mix.Pleroma
+
+  setup_all do
+    Mix.shell(Mix.Shell.Process)
+
+    on_exit(fn ->
+      Mix.shell(Mix.Shell.IO)
+    end)
+
+    :ok
+  end
+
+  describe "shell_prompt/1" do
+    test "input" do
+      send(self(), {:mix_shell_input, :prompt, "Yes"})
+
+      answer = shell_prompt("Do you want this?")
+      assert_received {:mix_shell, :prompt, [message]}
+      assert message =~ "Do you want this?"
+      assert answer == "Yes"
+    end
+
+    test "with defval" do
+      send(self(), {:mix_shell_input, :prompt, "\n"})
+
+      answer = shell_prompt("Do you want this?", "defval")
+
+      assert_received {:mix_shell, :prompt, [message]}
+      assert message =~ "Do you want this? [defval]"
+      assert answer == "defval"
+    end
+  end
+
+  describe "get_option/3" do
+    test "get from options" do
+      assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com"
+    end
+
+    test "get from prompt" do
+      send(self(), {:mix_shell_input, :prompt, "another-domain.com"})
+      assert get_option([], :domain, "Prompt") == "another-domain.com"
+    end
+  end
+end
diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs
new file mode 100644 (file)
index 0000000..539193f
--- /dev/null
@@ -0,0 +1,43 @@
+defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
+  use ExUnit.Case, async: true
+  alias Mix.Tasks.Pleroma.RobotsTxt
+
+  test "creates new dir" do
+    path = "test/fixtures/new_dir/"
+    file_path = path <> "robots.txt"
+
+    static_dir = Pleroma.Config.get([:instance, :static_dir])
+    Pleroma.Config.put([:instance, :static_dir], path)
+
+    on_exit(fn ->
+      Pleroma.Config.put([:instance, :static_dir], static_dir)
+      {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path)
+    end)
+
+    RobotsTxt.run(["disallow_all"])
+
+    assert File.exists?(file_path)
+    {:ok, file} = File.read(file_path)
+
+    assert file == "User-Agent: *\nDisallow: /\n"
+  end
+
+  test "to existance folder" do
+    path = "test/fixtures/"
+    file_path = path <> "robots.txt"
+    static_dir = Pleroma.Config.get([:instance, :static_dir])
+    Pleroma.Config.put([:instance, :static_dir], path)
+
+    on_exit(fn ->
+      Pleroma.Config.put([:instance, :static_dir], static_dir)
+      :ok = File.rm(file_path)
+    end)
+
+    RobotsTxt.run(["disallow_all"])
+
+    assert File.exists?(file_path)
+    {:ok, file} = File.read(file_path)
+
+    assert file == "User-Agent: *\nDisallow: /\n"
+  end
+end
index f604ba63d2c13fa138b1d530559f8a86be6f44d2..3e33f0335a89c03237346c421c134624591c92c2 100644 (file)
@@ -3,6 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 ExUnit.start()
-
 Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
+Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
 {:ok, _} = Application.ensure_all_started(:ex_machina)
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
new file mode 100644 (file)
index 0000000..1f01624
--- /dev/null
@@ -0,0 +1,252 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.UserSearchTest do
+  alias Pleroma.Repo
+  alias Pleroma.User
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  describe "User.search" do
+    test "accepts limit parameter" do
+      Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
+      assert length(User.search("john", limit: 3)) == 3
+      assert length(User.search("john")) == 5
+    end
+
+    test "accepts offset parameter" do
+      Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
+      assert length(User.search("john", limit: 3)) == 3
+      assert length(User.search("john", limit: 3, offset: 3)) == 2
+    end
+
+    test "finds a user by full or partial nickname" do
+      user = insert(:user, %{nickname: "john"})
+
+      Enum.each(["john", "jo", "j"], fn query ->
+        assert user ==
+                 User.search(query)
+                 |> List.first()
+                 |> Map.put(:search_rank, nil)
+                 |> Map.put(:search_type, nil)
+      end)
+    end
+
+    test "finds a user by full or partial name" do
+      user = insert(:user, %{name: "John Doe"})
+
+      Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
+        assert user ==
+                 User.search(query)
+                 |> List.first()
+                 |> Map.put(:search_rank, nil)
+                 |> Map.put(:search_type, nil)
+      end)
+    end
+
+    test "finds users, preferring nickname matches over name matches" do
+      u1 = insert(:user, %{name: "lain", nickname: "nick1"})
+      u2 = insert(:user, %{nickname: "lain", name: "nick1"})
+
+      assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
+    end
+
+    test "finds users, considering density of matched tokens" do
+      u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
+      u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
+
+      assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
+    end
+
+    test "finds users, ranking by similarity" do
+      u1 = insert(:user, %{name: "lain"})
+      _u2 = insert(:user, %{name: "ean"})
+      u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
+      u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
+
+      assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
+    end
+
+    test "finds users, handling misspelled requests" do
+      u1 = insert(:user, %{name: "lain"})
+
+      assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
+    end
+
+    test "finds users, boosting ranks of friends and followers" do
+      u1 = insert(:user)
+      u2 = insert(:user, %{name: "Doe"})
+      follower = insert(:user, %{name: "Doe"})
+      friend = insert(:user, %{name: "Doe"})
+
+      {:ok, follower} = User.follow(follower, u1)
+      {:ok, u1} = User.follow(u1, friend)
+
+      assert [friend.id, follower.id, u2.id] --
+               Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == []
+    end
+
+    test "finds followers of user by partial name" do
+      u1 = insert(:user)
+      u2 = insert(:user, %{name: "Jimi"})
+      follower_jimi = insert(:user, %{name: "Jimi Hendrix"})
+      follower_lizz = insert(:user, %{name: "Lizz Wright"})
+      friend = insert(:user, %{name: "Jimi"})
+
+      {:ok, follower_jimi} = User.follow(follower_jimi, u1)
+      {:ok, _follower_lizz} = User.follow(follower_lizz, u2)
+      {:ok, u1} = User.follow(u1, friend)
+
+      assert Enum.map(User.search("jimi", following: true, for_user: u1), & &1.id) == [
+               follower_jimi.id
+             ]
+
+      assert User.search("lizz", following: true, for_user: u1) == []
+    end
+
+    test "find local and remote users for authenticated users" do
+      u1 = insert(:user, %{name: "lain"})
+      u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+      u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+
+      results =
+        "lain"
+        |> User.search(for_user: u1)
+        |> Enum.map(& &1.id)
+        |> Enum.sort()
+
+      assert [u1.id, u2.id, u3.id] == results
+    end
+
+    test "find only local users for unauthenticated users" do
+      %{id: id} = insert(:user, %{name: "lain"})
+      insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+      insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+
+      assert [%{id: ^id}] = User.search("lain")
+    end
+
+    test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do
+      Pleroma.Config.put([:instance, :limit_to_local_content], :all)
+
+      %{id: id} = insert(:user, %{name: "lain"})
+      insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+      insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+
+      assert [%{id: ^id}] = User.search("lain")
+
+      Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+    end
+
+    test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do
+      Pleroma.Config.put([:instance, :limit_to_local_content], false)
+
+      u1 = insert(:user, %{name: "lain"})
+      u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
+      u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
+
+      results =
+        "lain"
+        |> User.search()
+        |> Enum.map(& &1.id)
+        |> Enum.sort()
+
+      assert [u1.id, u2.id, u3.id] == results
+
+      Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
+    end
+
+    test "finds a user whose name is nil" do
+      _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
+      user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
+
+      assert user_two ==
+               User.search("lain@pleroma.soykaf.com")
+               |> List.first()
+               |> Map.put(:search_rank, nil)
+               |> Map.put(:search_type, nil)
+    end
+
+    test "does not yield false-positive matches" do
+      insert(:user, %{name: "John Doe"})
+
+      Enum.each(["mary", "a", ""], fn query ->
+        assert [] == User.search(query)
+      end)
+    end
+
+    test "works with URIs" do
+      user = insert(:user)
+
+      results =
+        User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user)
+
+      result = results |> List.first()
+
+      user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
+
+      assert length(results) == 1
+      assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
+    end
+
+    test "excludes a blocked users from search result" do
+      user = insert(:user, %{nickname: "Bill"})
+
+      [blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"}))
+
+      blocked_user2 =
+        insert(
+          :user,
+          %{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"}
+        )
+
+      User.block_domain(user, "awful-and-rude-instance.com")
+      User.block(user, blocked_user)
+
+      account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids
+
+      assert account_ids == collect_ids(users)
+      refute Enum.member?(account_ids, blocked_user.id)
+      refute Enum.member?(account_ids, blocked_user2.id)
+      assert length(account_ids) == 3
+    end
+
+    test "local user has the same search_rank as for users with the same nickname, but another domain" do
+      user = insert(:user)
+      insert(:user, nickname: "lain@mastodon.social")
+      insert(:user, nickname: "lain")
+      insert(:user, nickname: "lain@pleroma.social")
+
+      assert User.search("lain@localhost", resolve: true, for_user: user)
+             |> Enum.each(fn u -> u.search_rank == 0.5 end)
+    end
+
+    test "localhost is the part of the domain" do
+      user = insert(:user)
+      insert(:user, nickname: "another@somedomain")
+      insert(:user, nickname: "lain")
+      insert(:user, nickname: "lain@examplelocalhost")
+
+      result = User.search("lain@examplelocalhost", resolve: true, for_user: user)
+      assert Enum.each(result, fn u -> u.search_rank == 0.5 end)
+      assert length(result) == 2
+    end
+
+    test "local user search with users" do
+      user = insert(:user)
+      local_user = insert(:user, nickname: "lain")
+      insert(:user, nickname: "another@localhost.com")
+      insert(:user, nickname: "localhost@localhost.com")
+
+      [result] = User.search("lain@localhost", resolve: true, for_user: user)
+      assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user
+    end
+  end
+end
index 198a97faec93676a4a6da168a2682558feaab005..fb497843c00f642eec02c5c2fe9591b75b48bea1 100644 (file)
@@ -1012,189 +1012,6 @@ defmodule Pleroma.UserTest do
     end
   end
 
-  describe "User.search" do
-    test "accepts limit parameter" do
-      Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
-      assert length(User.search("john", limit: 3)) == 3
-      assert length(User.search("john")) == 5
-    end
-
-    test "accepts offset parameter" do
-      Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
-      assert length(User.search("john", limit: 3)) == 3
-      assert length(User.search("john", limit: 3, offset: 3)) == 2
-    end
-
-    test "finds a user by full or partial nickname" do
-      user = insert(:user, %{nickname: "john"})
-
-      Enum.each(["john", "jo", "j"], fn query ->
-        assert user ==
-                 User.search(query)
-                 |> List.first()
-                 |> Map.put(:search_rank, nil)
-                 |> Map.put(:search_type, nil)
-      end)
-    end
-
-    test "finds a user by full or partial name" do
-      user = insert(:user, %{name: "John Doe"})
-
-      Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
-        assert user ==
-                 User.search(query)
-                 |> List.first()
-                 |> Map.put(:search_rank, nil)
-                 |> Map.put(:search_type, nil)
-      end)
-    end
-
-    test "finds users, preferring nickname matches over name matches" do
-      u1 = insert(:user, %{name: "lain", nickname: "nick1"})
-      u2 = insert(:user, %{nickname: "lain", name: "nick1"})
-
-      assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
-    end
-
-    test "finds users, considering density of matched tokens" do
-      u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
-      u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
-
-      assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
-    end
-
-    test "finds users, ranking by similarity" do
-      u1 = insert(:user, %{name: "lain"})
-      _u2 = insert(:user, %{name: "ean"})
-      u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
-      u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
-
-      assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
-    end
-
-    test "finds users, handling misspelled requests" do
-      u1 = insert(:user, %{name: "lain"})
-
-      assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
-    end
-
-    test "finds users, boosting ranks of friends and followers" do
-      u1 = insert(:user)
-      u2 = insert(:user, %{name: "Doe"})
-      follower = insert(:user, %{name: "Doe"})
-      friend = insert(:user, %{name: "Doe"})
-
-      {:ok, follower} = User.follow(follower, u1)
-      {:ok, u1} = User.follow(u1, friend)
-
-      assert [friend.id, follower.id, u2.id] --
-               Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == []
-    end
-
-    test "finds followers of user by partial name" do
-      u1 = insert(:user)
-      u2 = insert(:user, %{name: "Jimi"})
-      follower_jimi = insert(:user, %{name: "Jimi Hendrix"})
-      follower_lizz = insert(:user, %{name: "Lizz Wright"})
-      friend = insert(:user, %{name: "Jimi"})
-
-      {:ok, follower_jimi} = User.follow(follower_jimi, u1)
-      {:ok, _follower_lizz} = User.follow(follower_lizz, u2)
-      {:ok, u1} = User.follow(u1, friend)
-
-      assert Enum.map(User.search("jimi", following: true, for_user: u1), & &1.id) == [
-               follower_jimi.id
-             ]
-
-      assert User.search("lizz", following: true, for_user: u1) == []
-    end
-
-    test "find local and remote users for authenticated users" do
-      u1 = insert(:user, %{name: "lain"})
-      u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-      u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-
-      results =
-        "lain"
-        |> User.search(for_user: u1)
-        |> Enum.map(& &1.id)
-        |> Enum.sort()
-
-      assert [u1.id, u2.id, u3.id] == results
-    end
-
-    test "find only local users for unauthenticated users" do
-      %{id: id} = insert(:user, %{name: "lain"})
-      insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-      insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-
-      assert [%{id: ^id}] = User.search("lain")
-    end
-
-    test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do
-      Pleroma.Config.put([:instance, :limit_to_local_content], :all)
-
-      %{id: id} = insert(:user, %{name: "lain"})
-      insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-      insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-
-      assert [%{id: ^id}] = User.search("lain")
-
-      Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
-    end
-
-    test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do
-      Pleroma.Config.put([:instance, :limit_to_local_content], false)
-
-      u1 = insert(:user, %{name: "lain"})
-      u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
-      u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
-
-      results =
-        "lain"
-        |> User.search()
-        |> Enum.map(& &1.id)
-        |> Enum.sort()
-
-      assert [u1.id, u2.id, u3.id] == results
-
-      Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
-    end
-
-    test "finds a user whose name is nil" do
-      _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
-      user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
-
-      assert user_two ==
-               User.search("lain@pleroma.soykaf.com")
-               |> List.first()
-               |> Map.put(:search_rank, nil)
-               |> Map.put(:search_type, nil)
-    end
-
-    test "does not yield false-positive matches" do
-      insert(:user, %{name: "John Doe"})
-
-      Enum.each(["mary", "a", ""], fn query ->
-        assert [] == User.search(query)
-      end)
-    end
-
-    test "works with URIs" do
-      user = insert(:user)
-
-      results =
-        User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user)
-
-      result = results |> List.first()
-
-      user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
-
-      assert length(results) == 1
-      assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
-    end
-  end
-
   test "auth_active?/1 works correctly" do
     Pleroma.Config.put([:instance, :account_activation_required], true)
 
index 8b323372954bf9be125ac1658631a39c02fe2183..5a8a671552f18cf9bc218e4da47791e613ae49f3 100644 (file)
@@ -15,6 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    Pleroma.Config.put(config_path, true)
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+
     :ok
   end
 
index 68ec03c339616b5a4d2730c0194ca0c0fa2563ba..a914d3c4c77065462384820c36d7bf01003cb2c4 100644 (file)
@@ -11,12 +11,13 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.OStatus
   alias Pleroma.Web.Websub.WebsubClientSubscription
 
+  import Mock
   import Pleroma.Factory
   import ExUnit.CaptureLog
-  alias Pleroma.Web.CommonAPI
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -46,12 +47,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         data["object"]
         |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
 
-      data =
-        data
-        |> Map.put("object", object)
-
+      data = Map.put(data, "object", object)
       {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
-      returned_object = Object.normalize(returned_activity.data["object"])
+
+      returned_object = Object.normalize(returned_activity.data["object"], false)
 
       assert activity =
                Activity.get_create_by_object_ap_id(
@@ -61,6 +60,32 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
     end
 
+    test "it does not fetch replied-to activities beyond max_replies_depth" do
+      data =
+        File.read!("test/fixtures/mastodon-post-activity.json")
+        |> Poison.decode!()
+
+      object =
+        data["object"]
+        |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
+
+      data = Map.put(data, "object", object)
+
+      with_mock Pleroma.Web.Federator,
+        allowed_incoming_reply_depth?: fn _ -> false end do
+        {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
+
+        returned_object = Object.normalize(returned_activity.data["object"], false)
+
+        refute Activity.get_create_by_object_ap_id(
+                 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
+               )
+
+        assert returned_object.data["inReplyToAtomUri"] ==
+                 "https://shitposter.club/notice/2827873"
+      end
+    end
+
     test "it does not crash if the object in inReplyTo can't be fetched" do
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
index 0f43bc8f24e6d93c3857a9b265a66b80fcc365fc..69dd4d7473c4df10d78e638bb22537db48f03c0b 100644 (file)
@@ -12,6 +12,13 @@ defmodule Pleroma.Web.FederatorTest do
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    Pleroma.Config.put(config_path, true)
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+
     :ok
   end
 
index ec75150ab88c377099eaa7aefed0cef5a69ad732..73791a95b5f4fcfede475d03a13089ebd2647cc6 100644 (file)
@@ -444,4 +444,39 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
       assert Enum.at(result[:options], 2)[:votes_count] == 1
     end
   end
+
+  test "embeds a relationship in the account" do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{
+        "status" => "drink more water"
+      })
+
+    result = StatusView.render("status.json", %{activity: activity, for: other_user})
+
+    assert result[:account][:pleroma][:relationship] ==
+             AccountView.render("relationship.json", %{user: other_user, target: user})
+  end
+
+  test "embeds a relationship in the account in reposts" do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{
+        "status" => "˙˙ɐʎns"
+      })
+
+    {:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
+
+    result = StatusView.render("status.json", %{activity: activity, for: user})
+
+    assert result[:account][:pleroma][:relationship] ==
+             AccountView.render("relationship.json", %{user: user, target: other_user})
+
+    assert result[:reblog][:account][:pleroma][:relationship] ==
+             AccountView.render("relationship.json", %{user: user, target: user})
+  end
 end
index 7441e5fce9d5ab87ee10651750c96eed68e4d785..eae44dba5125e734841e96a8f0f865d369c58a74 100644 (file)
@@ -12,6 +12,13 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    Pleroma.Config.put(config_path, true)
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+
     :ok
   end
 
index f6be16862e34bb6864b2780390b1fa0625176189..acce330080dc394b05f81fe5785d49105ebaf672 100644 (file)
@@ -11,8 +11,10 @@ defmodule Pleroma.Web.OStatusTest do
   alias Pleroma.User
   alias Pleroma.Web.OStatus
   alias Pleroma.Web.XML
-  import Pleroma.Factory
+
   import ExUnit.CaptureLog
+  import Mock
+  import Pleroma.Factory
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -266,10 +268,13 @@ defmodule Pleroma.Web.OStatusTest do
     assert favorited_activity.local
   end
 
-  test "handle incoming replies" do
+  test_with_mock "handle incoming replies, fetching replied-to activities if we don't have them",
+                 OStatus,
+                 [:passthrough],
+                 [] do
     incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
     {:ok, [activity]} = OStatus.handle_incoming(incoming)
-    object = Object.normalize(activity.data["object"])
+    object = Object.normalize(activity.data["object"], false)
 
     assert activity.data["type"] == "Create"
     assert object.data["type"] == "Note"
@@ -282,6 +287,23 @@ defmodule Pleroma.Web.OStatusTest do
     assert object.data["id"] == "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
 
     assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
+
+    assert called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_))
+  end
+
+  test_with_mock "handle incoming replies, not fetching replied-to activities beyond max_replies_depth",
+                 OStatus,
+                 [:passthrough],
+                 [] do
+    incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
+
+    with_mock Pleroma.Web.Federator,
+      allowed_incoming_reply_depth?: fn _ -> false end do
+      {:ok, [activity]} = OStatus.handle_incoming(incoming)
+      object = Object.normalize(activity.data["object"], false)
+
+      refute called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_))
+    end
   end
 
   test "handle incoming follows" do
index 530562325f7a99664c48d6d2cfc9879448505987..c01e0112460b38397c8c554cae5e50117b694015 100644 (file)
@@ -5,6 +5,15 @@
 defmodule Pleroma.Web.FederatingPlugTest do
   use Pleroma.Web.ConnCase
 
+  setup_all do
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+
+    :ok
+  end
+
   test "returns and halt the conn when federating is disabled" do
     Pleroma.Config.put([:instance, :federating], false)
 
@@ -14,11 +23,11 @@ defmodule Pleroma.Web.FederatingPlugTest do
 
     assert conn.status == 404
     assert conn.halted
-
-    Pleroma.Config.put([:instance, :federating], true)
   end
 
   test "does nothing when federating is enabled" do
+    Pleroma.Config.put([:instance, :federating], true)
+
     conn =
       build_conn()
       |> Pleroma.Web.FederatingPlug.call(%{})
index 43fccfc7ab5241f948027589d807d9b66c821a94..a14ed3126d43395e298b95df87125ef79574e333 100644 (file)
@@ -10,6 +10,12 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
 
   setup do
     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    Pleroma.Config.put(config_path, true)
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
     :ok
   end
 
index f79745d58ce340dd0691beafe84bf5f033e05d34..aa7262beb1eb3c6d2a0cec2dabdead769e2523c2 100644 (file)
@@ -9,6 +9,16 @@ defmodule Pleroma.Web.Websub.WebsubControllerTest do
   alias Pleroma.Web.Websub
   alias Pleroma.Web.Websub.WebsubClientSubscription
 
+  setup_all do
+    config_path = [:instance, :federating]
+    initial_setting = Pleroma.Config.get(config_path)
+
+    Pleroma.Config.put(config_path, true)
+    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+
+    :ok
+  end
+
   test "websub subscription request", %{conn: conn} do
     user = insert(:user)