diff options
Diffstat (limited to 'lib')
33 files changed, 460 insertions, 267 deletions
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 2ae052069..e17068876 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Application do    def start(_type, _args) do      Pleroma.HTML.compile_scrubbers()      Pleroma.Config.DeprecationWarnings.warn() +    Pleroma.Repo.check_migrations_applied!()      setup_instrumenters()      load_custom_modules() diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index aafe57280..e5d28ebff 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -64,11 +64,13 @@ defmodule Pleroma.Conversation.Participation do    end    def mark_as_read(participation) do -    participation -    |> read_cng(%{read: true}) -    |> Repo.update() +    __MODULE__ +    |> where(id: ^participation.id) +    |> update(set: [read: true]) +    |> select([p], p) +    |> Repo.update_all([])      |> case do -      {:ok, participation} -> +      {1, [participation]} ->          participation = Repo.preload(participation, :user)          User.set_unread_conversation_count(participation.user)          {:ok, participation} diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index eb37b95a6..2452a7389 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Object do    require Logger +  @type t() :: %__MODULE__{} +    schema "objects" do      field(:data, :map) @@ -79,6 +81,20 @@ defmodule Pleroma.Object do      Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))    end +  @doc """ +  Get a single attachment by it's name and href +  """ +  @spec get_attachment_by_name_and_href(String.t(), String.t()) :: Object.t() | nil +  def get_attachment_by_name_and_href(name, href) do +    query = +      from(o in Object, +        where: fragment("(?)->>'name' = ?", o.data, ^name), +        where: fragment("(?)->>'href' = ?", o.data, ^href) +      ) + +    Repo.one(query) +  end +    defp warn_on_no_object_preloaded(ap_id) do      "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"      |> Logger.debug() @@ -164,6 +180,7 @@ defmodule Pleroma.Object do    def delete(%Object{data: %{"id" => id}} = object) do      with {:ok, _obj} = swap_object_with_tombstone(object), +         :ok <- delete_attachments(object),           deleted_activity = Activity.delete_all_by_object_ap_id(id),           {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),           {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do @@ -171,6 +188,77 @@ defmodule Pleroma.Object do      end    end +  defp delete_attachments(%{data: %{"attachment" => [_ | _] = attachments, "actor" => actor}}) do +    hrefs = +      Enum.flat_map(attachments, fn attachment -> +        Enum.map(attachment["url"], & &1["href"]) +      end) + +    names = Enum.map(attachments, & &1["name"]) + +    uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + +    # find all objects for copies of the attachments, name and actor doesn't matter here +    delete_ids = +      from(o in Object, +        where: +          fragment( +            "to_jsonb(array(select jsonb_array_elements((?)#>'{url}') ->> 'href'))::jsonb \\?| (?)", +            o.data, +            ^hrefs +          ) +      ) +      |> Repo.all() +      # we should delete 1 object for any given attachment, but don't delete files if +      # there are more than 1 object for it +      |> Enum.reduce(%{}, fn %{ +                               id: id, +                               data: %{ +                                 "url" => [%{"href" => href}], +                                 "actor" => obj_actor, +                                 "name" => name +                               } +                             }, +                             acc -> +        Map.update(acc, href, %{id: id, count: 1}, fn val -> +          case obj_actor == actor and name in names do +            true -> +              # set id of the actor's object that will be deleted +              %{val | id: id, count: val.count + 1} + +            false -> +              # another actor's object, just increase count to not delete file +              %{val | count: val.count + 1} +          end +        end) +      end) +      |> Enum.map(fn {href, %{id: id, count: count}} -> +        # only delete files that have single instance +        with 1 <- count do +          prefix = +            case Pleroma.Config.get([Pleroma.Upload, :base_url]) do +              nil -> "media" +              _ -> "" +            end + +          base_url = Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url()) + +          file_path = String.trim_leading(href, "#{base_url}/#{prefix}") + +          uploader.delete_file(file_path) +        end + +        id +      end) + +    from(o in Object, where: o.id in ^delete_ids) +    |> Repo.delete_all() + +    :ok +  end + +  defp delete_attachments(%{data: _data}), do: :ok +    def prune(%Object{data: %{"id" => id}} = object) do      with {:ok, object} <- Repo.delete(object),           {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index 174a8389c..07c0f7fdb 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -18,16 +18,13 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do      token = assigns[:token]      scopes = transform_scopes(scopes, options) -    matched_scopes = token && filter_descendants(scopes, token.scopes) +    matched_scopes = (token && filter_descendants(scopes, token.scopes)) || []      cond do -      is_nil(token) -> -        maybe_perform_instance_privacy_check(conn, options) - -      op == :| && Enum.any?(matched_scopes) -> +      token && op == :| && Enum.any?(matched_scopes) ->          conn -      op == :& && matched_scopes == scopes -> +      token && op == :& && matched_scopes == scopes ->          conn        options[:fallback] == :proceed_unauthenticated -> diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 582fb1f92..3190163d3 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -23,6 +23,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do        token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->          # Note: checking for _any_ admin scope presence, not necessarily fitting requested action.          #   Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. +        #   Admin might opt out of admin scope for some apps to block any admin actions from them.          conn        true -> diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index f57e088bc..cb0b6653c 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -8,6 +8,8 @@ defmodule Pleroma.Repo do      adapter: Ecto.Adapters.Postgres,      migration_timestamps: [type: :naive_datetime_usec] +  require Logger +    defmodule Instrumenter do      use Prometheus.EctoInstrumenter    end @@ -47,4 +49,37 @@ defmodule Pleroma.Repo do        _ -> {:error, :not_found}      end    end + +  def check_migrations_applied!() do +    unless Pleroma.Config.get( +             [:i_am_aware_this_may_cause_data_loss, :disable_migration_check], +             false +           ) do +      Ecto.Migrator.with_repo(__MODULE__, fn repo -> +        down_migrations = +          Ecto.Migrator.migrations(repo) +          |> Enum.reject(fn +            {:up, _, _} -> true +            {:down, _, _} -> false +          end) + +        if length(down_migrations) > 0 do +          down_migrations_text = +            Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end) + +          Logger.error( +            "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" +          ) + +          raise Pleroma.Repo.UnappliedMigrationsError +        end +      end) +    else +      :ok +    end +  end +end + +defmodule Pleroma.Repo.UnappliedMigrationsError do +  defexception message: "Unapplied Migrations detected"  end diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex index 36b3c35ec..2e6fe3292 100644 --- a/lib/pleroma/uploaders/local.ex +++ b/lib/pleroma/uploaders/local.ex @@ -5,10 +5,12 @@  defmodule Pleroma.Uploaders.Local do    @behaviour Pleroma.Uploaders.Uploader +  @impl true    def get_file(_) do      {:ok, {:static_dir, upload_path()}}    end +  @impl true    def put_file(upload) do      {local_path, file} =        case Enum.reverse(Path.split(upload.path)) do @@ -33,4 +35,15 @@ defmodule Pleroma.Uploaders.Local do    def upload_path do      Pleroma.Config.get!([__MODULE__, :uploads])    end + +  @impl true +  def delete_file(path) do +    upload_path() +    |> Path.join(path) +    |> File.rm() +    |> case do +      :ok -> :ok +      {:error, posix_error} -> {:error, to_string(posix_error)} +    end +  end  end diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex deleted file mode 100644 index c36f3d61d..000000000 --- a/lib/pleroma/uploaders/mdii.ex +++ /dev/null @@ -1,37 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Uploaders.MDII do -  @moduledoc "Represents uploader for https://github.com/hakaba-hitoyo/minimal-digital-image-infrastructure" - -  alias Pleroma.Config -  alias Pleroma.HTTP - -  @behaviour Pleroma.Uploaders.Uploader - -  # MDII-hosted images are never passed through the MediaPlug; only local media. -  # Delegate to Pleroma.Uploaders.Local -  def get_file(file) do -    Pleroma.Uploaders.Local.get_file(file) -  end - -  def put_file(upload) do -    cgi = Config.get([Pleroma.Uploaders.MDII, :cgi]) -    files = Config.get([Pleroma.Uploaders.MDII, :files]) - -    {:ok, file_data} = File.read(upload.tempfile) - -    extension = String.split(upload.name, ".") |> List.last() -    query = "#{cgi}?#{extension}" - -    with {:ok, %{status: 200, body: body}} <- -           HTTP.post(query, file_data, [], adapter: [pool: :default]) do -      remote_file_name = String.split(body) |> List.first() -      public_url = "#{files}/#{remote_file_name}.#{extension}" -      {:ok, {:url, public_url}} -    else -      _ -> Pleroma.Uploaders.Local.put_file(upload) -    end -  end -end diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex index 9876b6398..feb89cea6 100644 --- a/lib/pleroma/uploaders/s3.ex +++ b/lib/pleroma/uploaders/s3.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Uploaders.S3 do    # The file name is re-encoded with S3's constraints here to comply with previous    # links with less strict filenames +  @impl true    def get_file(file) do      config = Config.get([__MODULE__])      bucket = Keyword.fetch!(config, :bucket) @@ -35,6 +36,7 @@ defmodule Pleroma.Uploaders.S3 do        ])}}    end +  @impl true    def put_file(%Pleroma.Upload{} = upload) do      config = Config.get([__MODULE__])      bucket = Keyword.get(config, :bucket) @@ -69,6 +71,18 @@ defmodule Pleroma.Uploaders.S3 do      end    end +  @impl true +  def delete_file(file) do +    [__MODULE__, :bucket] +    |> Config.get() +    |> ExAws.S3.delete_object(file) +    |> ExAws.request() +    |> case do +      {:ok, %{status_code: 204}} -> :ok +      error -> {:error, inspect(error)} +    end +  end +    @regex Regex.compile!("[^0-9a-zA-Z!.*/'()_-]")    def strict_encode(name) do      String.replace(name, @regex, "-") diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex index c0b22c28a..d71e213d2 100644 --- a/lib/pleroma/uploaders/uploader.ex +++ b/lib/pleroma/uploaders/uploader.ex @@ -36,6 +36,8 @@ defmodule Pleroma.Uploaders.Uploader do    @callback put_file(Pleroma.Upload.t()) ::                :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback +  @callback delete_file(file :: String.t()) :: :ok | {:error, String.t()} +    @callback http_callback(Plug.Conn.t(), Map.t()) ::                {:ok, Plug.Conn.t()}                | {:ok, Plug.Conn.t(), file_spec()} @@ -43,7 +45,6 @@ defmodule Pleroma.Uploaders.Uploader do    @optional_callbacks http_callback: 2    @spec put_file(module(), Pleroma.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()} -    def put_file(uploader, upload) do      case uploader.put_file(upload) do        :ok -> {:ok, {:file, upload.path}} diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 706aee2ff..430f04ae9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1430,20 +1430,47 @@ defmodule Pleroma.User do    Creates an internal service actor by URI if missing.    Optionally takes nickname for addressing.    """ -  def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do -    with user when is_nil(user) <- get_cached_by_ap_id(uri) do -      {:ok, user} = -        %User{ -          invisible: true, -          local: true, -          ap_id: uri, -          nickname: nickname, -          follower_address: uri <> "/followers" -        } -        |> Repo.insert() +  @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil +  def get_or_create_service_actor_by_ap_id(uri, nickname) do +    {_, user} = +      case get_cached_by_ap_id(uri) do +        nil -> +          with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do +            Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}") +            {:error, nil} +          end -      user -    end +        %User{invisible: false} = user -> +          set_invisible(user) + +        user -> +          {:ok, user} +      end + +    user +  end + +  @spec set_invisible(User.t()) :: {:ok, User.t()} +  defp set_invisible(user) do +    user +    |> change(%{invisible: true}) +    |> update_and_set_cache() +  end + +  @spec create_service_actor(String.t(), String.t()) :: +          {:ok, User.t()} | {:error, Ecto.Changeset.t()} +  defp create_service_actor(uri, nickname) do +    %User{ +      invisible: true, +      local: true, +      ap_id: uri, +      nickname: nickname, +      follower_address: uri <> "/followers" +    } +    |> change +    |> unique_constraint(:nickname) +    |> Repo.insert() +    |> set_cache()    end    # AP style @@ -1847,22 +1874,13 @@ defmodule Pleroma.User do    end    def admin_api_update(user, params) do -    changeset = -      cast(user, params, [ -        :is_moderator, -        :is_admin, -        :show_role -      ]) - -    with {:ok, updated_user} <- update_and_set_cache(changeset) do -      if user.is_admin && !updated_user.is_admin do -        # Tokens & authorizations containing any admin scopes must be revoked (revoking all). -        # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins). -        global_sign_out(user) -      end - -      {:ok, updated_user} -    end +    user +    |> cast(params, [ +      :is_moderator, +      :is_admin, +      :show_role +    ]) +    |> update_and_set_cache()    end    @doc "Signs user out of all applications" diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index db072bad2..e4e3ab44a 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -264,6 +264,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do          "rel" => "self",          "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",          "href" => user.ap_id +      }, +      %{ +        "rel" => "http://ostatus.org/schema/1.0/subscribe", +        "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}"        }      ]    end diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 99a804568..48a1b71e0 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -9,10 +9,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do    alias Pleroma.Web.ActivityPub.ActivityPub    require Logger +  @relay_nickname "relay" +    def get_actor do      actor =        relay_ap_id() -      |> User.get_or_create_service_actor_by_ap_id() +      |> User.get_or_create_service_actor_by_ap_id(@relay_nickname)      actor    end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3fa789d53..2b8bfc3bd 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -658,24 +658,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do        {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) -      locked = new_user_data[:locked] || false -      attachment = get_in(new_user_data, [:source_data, "attachment"]) || [] -      invisible = new_user_data[:invisible] || false - -      fields = -        attachment -        |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) -        |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) - -      update_data = -        new_user_data -        |> Map.take([:avatar, :banner, :bio, :name, :also_known_as]) -        |> Map.put(:fields, fields) -        |> Map.put(:locked, locked) -        |> Map.put(:invisible, invisible) -        actor -      |> User.upgrade_changeset(update_data, true) +      |> User.upgrade_changeset(new_user_data, true)        |> User.update_and_set_cache()        ActivityPub.update(%{ diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index ddae139c6..c9d7ab867 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -32,19 +32,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    plug(      OAuthScopesPlug,      %{scopes: ["read:accounts"], admin: true} -    when action in [:list_users, :user_show, :right_get, :invites] +    when action in [:list_users, :user_show, :right_get]    )    plug(      OAuthScopesPlug,      %{scopes: ["write:accounts"], admin: true}      when action in [ -           :get_invite_token, -           :revoke_invite, -           :email_invite,             :get_password_reset, -           :user_follow, -           :user_unfollow,             :user_delete,             :users_create,             :user_toggle_activation, @@ -57,6 +52,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do           ]    ) +  plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites) + +  plug( +    OAuthScopesPlug, +    %{scopes: ["write:invites"], admin: true} +    when action in [:create_invite_token, :revoke_invite, :email_invite] +  ) + +  plug( +    OAuthScopesPlug, +    %{scopes: ["write:follows"], admin: true} +    when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] +  ) +    plug(      OAuthScopesPlug,      %{scopes: ["read:reports"], admin: true} @@ -90,7 +99,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    plug(      OAuthScopesPlug,      %{scopes: ["write"], admin: true} -    when action in [:relay_follow, :relay_unfollow, :config_update] +    when action == :config_update    )    @users_page_size 50 diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index ca261ad6e..9f7e4943c 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -20,18 +20,21 @@ defmodule Pleroma.Web.MastoFEController do    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)    @doc "GET /web/*path" -  def index(%{assigns: %{user: user}} = conn, _params) do -    token = get_session(conn, :oauth_token) +  def index(%{assigns: %{user: user, token: token}} = conn, _params) +      when not is_nil(user) and not is_nil(token) do +    conn +    |> put_layout(false) +    |> render("index.html", +      token: token.token, +      user: user, +      custom_emojis: Pleroma.Emoji.get_all() +    ) +  end -    if user && token do -      conn -      |> put_layout(false) -      |> render("index.html", token: token, user: user, custom_emojis: Pleroma.Emoji.get_all()) -    else -      conn -      |> put_session(:return_to, conn.request_path) -      |> redirect(to: "/web/login") -    end +  def index(conn, _params) do +    conn +    |> put_session(:return_to, conn.request_path) +    |> redirect(to: "/web/login")    end    @doc "GET /web/manifest.json" diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 16759be6a..f2508aca4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -23,6 +23,23 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do    plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)    # GET /api/v1/notifications +  def index(conn, %{"account_id" => account_id} = params) do +    case Pleroma.User.get_cached_by_id(account_id) do +      %{ap_id: account_ap_id} -> +        params = +          params +          |> Map.delete("account_id") +          |> Map.put("account_ap_id", account_ap_id) + +        index(conn, params) + +      _ -> +        conn +        |> put_status(:not_found) +        |> json(%{"error" => "Account is not found"}) +    end +  end +    def index(%{assigns: %{user: user}} = conn, params) do      notifications = MastodonAPI.get_notifications(user, params) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 384159336..29964a1d4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -77,10 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do      |> render("index.json", activities: activities, for: user, as: :activity)    end -  # GET /api/v1/timelines/tag/:tag -  def hashtag(%{assigns: %{user: user}} = conn, params) do -    local_only = truthy_param?(params["local"]) - +  def hashtag_fetching(params, user, local_only) do      tags =        [params["tag"], params["any"]]        |> List.flatten() @@ -98,7 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do        |> Map.get("none", [])        |> Enum.map(&String.downcase(&1)) -    activities = +    _activities =        params        |> Map.put("type", "Create")        |> Map.put("local_only", local_only) @@ -109,6 +106,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do        |> Map.put("tag_all", tag_all)        |> Map.put("tag_reject", tag_reject)        |> ActivityPub.fetch_public_activities() +  end + +  # GET /api/v1/timelines/tag/:tag +  def hashtag(%{assigns: %{user: user}} = conn, params) do +    local_only = truthy_param?(params["local"]) + +    activities = hashtag_fetching(params, user, local_only)      conn      |> add_link_headers(activities, %{"local" => local_only}) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index b1816370e..390a2b190 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -56,6 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do      user      |> Notification.for_user_query(options)      |> restrict(:exclude_types, options) +    |> restrict(:account_ap_id, options)      |> Pagination.fetch_paginated(params)    end @@ -71,7 +72,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do        exclude_visibilities: {:array, :string},        reblogs: :boolean,        with_muted: :boolean, -      with_move: :boolean +      with_move: :boolean, +      account_ap_id: :string      }      changeset = cast({%{}, param_types}, params, Map.keys(param_types)) @@ -88,5 +90,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do      |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))    end +  defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do +    where(query, [n, a], a.actor == ^account_ap_id) +  end +    defp restrict(query, _, _), do: query  end diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 382ecf426..589d11901 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Web.Metadata.Utils do      |> String.replace(~r/<br\s?\/?>/, " ")      |> HTML.get_cached_stripped_html_for_activity(object, "metadata")      |> Emoji.Formatter.demojify() +    |> HtmlEntities.decode()      |> Formatter.truncate()    end @@ -25,6 +26,7 @@ defmodule Pleroma.Web.Metadata.Utils do      |> String.replace(~r/<br\s?\/?>/, " ")      |> HTML.strip_tags()      |> Emoji.Formatter.demojify() +    |> HtmlEntities.decode()      |> Formatter.truncate(max_length)    end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 87acdec97..d31a3d91c 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -222,7 +222,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:user_active, true} <- {:user_active, !user.deactivated},           {:password_reset_pending, false} <-             {:password_reset_pending, user.password_reset_pending}, -         {:ok, scopes} <- validate_scopes(app, params, user), +         {:ok, scopes} <- validate_scopes(app, params),           {:ok, auth} <- Authorization.create_authorization(app, user, scopes),           {:ok, token} <- Token.exchange_token(app, auth) do        json(conn, Token.Response.build(user, token)) @@ -471,7 +471,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do             {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},           %App{} = app <- Repo.get_by(App, client_id: client_id),           true <- redirect_uri in String.split(app.redirect_uris), -         {:ok, scopes} <- validate_scopes(app, auth_attrs, user), +         {:ok, scopes} <- validate_scopes(app, auth_attrs),           {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do        Authorization.create_authorization(app, user, scopes)      end @@ -487,12 +487,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do    defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),      do: put_session(conn, :registration_id, registration_id) -  @spec validate_scopes(App.t(), map(), User.t()) :: +  @spec validate_scopes(App.t(), map()) ::            {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} -  defp validate_scopes(%App{} = app, params, %User{} = user) do +  defp validate_scopes(%App{} = app, params) do      params      |> Scopes.fetch_scopes(app.scopes) -    |> Scopes.validate(app.scopes, user) +    |> Scopes.validate(app.scopes)    end    def default_redirect_uri(%App{} = app) do diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 00da225b9..151467494 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.OAuth.Scopes do    """    alias Pleroma.Plugs.OAuthScopesPlug -  alias Pleroma.User    @doc """    Fetch scopes from request params. @@ -56,35 +55,18 @@ defmodule Pleroma.Web.OAuth.Scopes do    @doc """    Validates scopes.    """ -  @spec validate(list() | nil, list(), User.t()) :: +  @spec validate(list() | nil, list()) ::            {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} -  def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []], +  def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []],      do: {:error, :missing_scopes} -  def validate(scopes, app_scopes, %User{} = user) do -    with {:ok, _} <- ensure_scopes_support(scopes, app_scopes), -         {:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do -      {:ok, scopes} -    end -  end - -  defp ensure_scopes_support(scopes, app_scopes) do +  def validate(scopes, app_scopes) do      case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do        ^scopes -> {:ok, scopes}        _ -> {:error, :unsupported_scopes}      end    end -  defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do -    if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do -      {:ok, scopes} -    else -      # Gracefully dropping admin scopes from requested scopes if user isn't an admin (not raising) -      scopes = scopes -- OAuthScopesPlug.filter_descendants(scopes, ["admin"]) -      validate(scopes, app_scopes, user) -    end -  end -    def contains_admin_scopes?(scopes) do      scopes      |> OAuthScopesPlug.filter_descendants(["admin"]) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 69dfa92e3..0bbf84fd3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do    @doc """    Lists the packs available on the instance as JSON. -  The information is public and does not require authentification. The format is +  The information is public and does not require authentication. The format is    a map of "pack directory name" to pack.json contents.    """    def list_packs(conn, _params) do diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 8fed3f5bb..772c535a4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -22,7 +22,14 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do    plug(      OAuthScopesPlug, -    %{scopes: ["read:statuses"]} when action in [:conversation, :conversation_statuses] +    %{scopes: ["read:statuses"]} +    when action in [:conversation, :conversation_statuses, :emoji_reactions_by] +  ) + +  plug( +    OAuthScopesPlug, +    %{scopes: ["write:statuses"]} +    when action in [:react_with_emoji, :unreact_with_emoji]    )    plug( diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index f6c128283..9654ab8a3 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -229,9 +229,9 @@ defmodule Pleroma.Web.Router do      pipe_through(:pleroma_html)      post("/main/ostatus", UtilController, :remote_subscribe) -    get("/ostatus_subscribe", UtilController, :remote_follow) +    get("/ostatus_subscribe", RemoteFollowController, :follow) -    post("/ostatus_subscribe", UtilController, :do_remote_follow) +    post("/ostatus_subscribe", RemoteFollowController, :do_follow)    end    scope "/api/pleroma", Pleroma.Web.TwitterAPI do diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex new file mode 100644 index 000000000..5ba192cd7 --- /dev/null +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex @@ -0,0 +1,11 @@ +<%= if @error == :error do %> +    <h2>Error fetching user</h2> +<% else %> +    <h2>Remote follow</h2> +    <img height="128" width="128" src="<%= avatar_url(@followee) %>"> +    <p><%= @followee.nickname %></p> +    <%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %> +    <%= hidden_input f, :id, value: @followee.id %> +    <%= submit "Authorize" %> +    <% end %> +<% end %> diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex new file mode 100644 index 000000000..df44988ee --- /dev/null +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex @@ -0,0 +1,14 @@ +<%= if @error do %> +<h2><%= @error %></h2> +<% end %> +<h2>Log in to follow</h2> +<p><%= @followee.nickname %></p> +<img height="128" width="128" src="<%= avatar_url(@followee) %>"> +<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %> +<%= text_input f, :name, placeholder: "Username", required: true %> +<br> +<%= password_input f, :password, placeholder: "Password", required: true %> +<br> +<%= hidden_input f, :id, value: @followee.id %> +<%= submit "Authorize" %> +<% end %> diff --git a/lib/pleroma/web/templates/twitter_api/util/followed.html.eex b/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex index da473d502..da473d502 100644 --- a/lib/pleroma/web/templates/twitter_api/util/followed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/follow.html.eex b/lib/pleroma/web/templates/twitter_api/util/follow.html.eex deleted file mode 100644 index 06359fa6c..000000000 --- a/lib/pleroma/web/templates/twitter_api/util/follow.html.eex +++ /dev/null @@ -1,11 +0,0 @@ -<%= if @error == :error do %> -    <h2>Error fetching user</h2> -<% else %> -    <h2>Remote follow</h2> -    <img width="128" height="128" src="<%= @avatar %>"> -    <p><%= @name %></p> -    <%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "user"], fn f -> %> -    <%= hidden_input f, :id, value: @id %> -    <%= submit "Authorize" %> -    <% end %> -<% end %> diff --git a/lib/pleroma/web/templates/twitter_api/util/follow_login.html.eex b/lib/pleroma/web/templates/twitter_api/util/follow_login.html.eex deleted file mode 100644 index 4e3a2be67..000000000 --- a/lib/pleroma/web/templates/twitter_api/util/follow_login.html.eex +++ /dev/null @@ -1,14 +0,0 @@ -<%= if @error do %> -    <h2><%= @error %></h2> -<% end %> -<h2>Log in to follow</h2> -<p><%= @name %></p> -<img height="128" width="128" src="<%= @avatar %>"> -<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "authorization"], fn f -> %> -<%= text_input f, :name, placeholder: "Username" %> -<br> -<%= password_input f, :password, placeholder: "Password" %> -<br> -<%= hidden_input f, :id, value: @id %> -<%= submit "Authorize" %> -<% end %> diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex new file mode 100644 index 000000000..e0d4d5632 --- /dev/null +++ b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex @@ -0,0 +1,112 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do +  use Pleroma.Web, :controller + +  require Logger + +  alias Pleroma.Activity +  alias Pleroma.Object.Fetcher +  alias Pleroma.Plugs.OAuthScopesPlug +  alias Pleroma.User +  alias Pleroma.Web.Auth.Authenticator +  alias Pleroma.Web.CommonAPI + +  @status_types ["Article", "Event", "Note", "Video", "Page", "Question"] + +  # Note: follower can submit the form (with password auth) not being signed in (having no token) +  plug( +    OAuthScopesPlug, +    %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} +    when action in [:do_follow] +  ) + +  # GET /ostatus_subscribe +  # +  def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do +    case is_status?(acct) do +      true -> follow_status(conn, user, acct) +      _ -> follow_account(conn, user, acct) +    end +  end + +  defp follow_status(conn, _user, acct) do +    with {:ok, object} <- Fetcher.fetch_object_from_id(acct), +         %Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object.data["id"]) do +      redirect(conn, to: o_status_path(conn, :notice, activity_id)) +    else +      error -> +        handle_follow_error(conn, error) +    end +  end + +  defp follow_account(conn, user, acct) do +    with {:ok, followee} <- User.get_or_fetch(acct) do +      render(conn, follow_template(user), %{error: false, followee: followee, acct: acct}) +    else +      {:error, _reason} -> +        render(conn, follow_template(user), %{error: :error}) +    end +  end + +  defp follow_template(%User{} = _user), do: "follow.html" +  defp follow_template(_), do: "follow_login.html" + +  defp is_status?(acct) do +    case Fetcher.fetch_and_contain_remote_object_from_id(acct) do +      {:ok, %{"type" => type}} when type in @status_types -> +        true + +      _ -> +        false +    end +  end + +  # POST  /ostatus_subscribe +  # +  def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do +    with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, +         {:ok, _, _, _} <- CommonAPI.follow(user, followee) do +      render(conn, "followed.html", %{error: false}) +    else +      error -> +        handle_follow_error(conn, error) +    end +  end + +  def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do +    with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, +         {_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee}, +         {:ok, _, _, _} <- CommonAPI.follow(user, followee) do +      render(conn, "followed.html", %{error: false}) +    else +      error -> +        handle_follow_error(conn, error) +    end +  end + +  def do_follow(%{assigns: %{user: nil}} = conn, _) do +    Logger.debug("Insufficient permissions: follow | write:follows.") +    render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."}) +  end + +  defp handle_follow_error(conn, {:auth, _, followee} = _) do +    render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee}) +  end + +  defp handle_follow_error(conn, {:fetch_user, error} = _) do +    Logger.debug("Remote follow failed with error #{inspect(error)}") +    render(conn, "followed.html", %{error: "Could not find user"}) +  end + +  defp handle_follow_error(conn, {:error, "Could not follow user:" <> _} = _) do +    render(conn, "followed.html", %{error: "Error following account"}) +  end + +  defp handle_follow_error(conn, error) do +    Logger.debug("Remote follow failed with error #{inspect(error)}") +    render(conn, "followed.html", %{error: "Something went wrong."}) +  end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 799dd17ae..f08b9d28c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -7,12 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    require Logger -  alias Pleroma.Activity    alias Pleroma.Config    alias Pleroma.Emoji    alias Pleroma.Healthcheck    alias Pleroma.Notification -  alias Pleroma.Plugs.AuthenticationPlug    alias Pleroma.Plugs.OAuthScopesPlug    alias Pleroma.User    alias Pleroma.Web @@ -22,7 +20,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    plug(      OAuthScopesPlug,      %{scopes: ["follow", "write:follows"]} -    when action in [:do_remote_follow, :follow_import] +    when action == :follow_import +  ) + +  # Note: follower can submit the form (with password auth) not being signed in (having no token) +  plug( +    OAuthScopesPlug, +    %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} +    when action == :do_remote_follow    )    plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import) @@ -77,95 +82,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do      end    end -  def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do -    if is_status?(acct) do -      {:ok, object} = Pleroma.Object.Fetcher.fetch_object_from_id(acct) -      %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) -      redirect(conn, to: "/notice/#{activity_id}") -    else -      with {:ok, followee} <- User.get_or_fetch(acct) do -        conn -        |> render(follow_template(user), %{ -          error: false, -          acct: acct, -          avatar: User.avatar_url(followee), -          name: followee.nickname, -          id: followee.id -        }) -      else -        {:error, _reason} -> -          render(conn, follow_template(user), %{error: :error}) -      end -    end -  end - -  defp follow_template(%User{} = _user), do: "follow.html" -  defp follow_template(_), do: "follow_login.html" - -  defp is_status?(acct) do -    case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do -      {:ok, %{"type" => type}} -      when type in ["Article", "Event", "Note", "Video", "Page", "Question"] -> -        true - -      _ -> -        false -    end -  end - -  def do_remote_follow(conn, %{ -        "authorization" => %{"name" => username, "password" => password, "id" => id} -      }) do -    with %User{} = followee <- User.get_cached_by_id(id), -         {_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee}, -         {_, true, _} <- { -           :auth, -           AuthenticationPlug.checkpw(password, user.password_hash), -           followee -         }, -         {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do -      conn -      |> render("followed.html", %{error: false}) -    else -      # Was already following user -      {:error, "Could not follow user:" <> _rest} -> -        render(conn, "followed.html", %{error: "Error following account"}) - -      {:auth, _, followee} -> -        conn -        |> render("follow_login.html", %{ -          error: "Wrong username or password", -          id: id, -          name: followee.nickname, -          avatar: User.avatar_url(followee) -        }) - -      e -> -        Logger.debug("Remote follow failed with error #{inspect(e)}") -        render(conn, "followed.html", %{error: "Something went wrong."}) -    end -  end - -  def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do -    with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, -         {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do -      conn -      |> render("followed.html", %{error: false}) -    else -      # Was already following user -      {:error, "Could not follow user:" <> _rest} -> -        render(conn, "followed.html", %{error: "Error following account"}) - -      {:fetch_user, error} -> -        Logger.debug("Remote follow failed with error #{inspect(error)}") -        render(conn, "followed.html", %{error: "Could not find user"}) - -      e -> -        Logger.debug("Remote follow failed with error #{inspect(e)}") -        render(conn, "followed.html", %{error: "Something went wrong."}) -    end -  end -    def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do      with {:ok, _} <- Notification.read_one(user, notification_id) do        json(conn, %{status: "success"}) @@ -346,7 +262,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do    end    def delete_account(%{assigns: %{user: user}} = conn, params) do -    case CommonAPI.Utils.confirm_current_password(user, params["password"]) do +    password = params["password"] || "" + +    case CommonAPI.Utils.confirm_current_password(user, password) do        {:ok, user} ->          User.delete(user)          json(conn, %{status: "success"}) diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex new file mode 100644 index 000000000..d469c4726 --- /dev/null +++ b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do +  use Pleroma.Web, :view +  import Phoenix.HTML.Form + +  defdelegate avatar_url(user), to: Pleroma.User +end  | 
