Merge branch 'develop' into feature/compat/push-subscriptions

# Conflicts:
#	lib/pleroma/application.ex
#	lib/pleroma/plugs/oauth_plug.ex
This commit is contained in:
Egor Kislitsyn 2018-12-06 20:15:16 +07:00
commit 4944498133
119 changed files with 3675 additions and 1906 deletions

5
.gitignore vendored
View file

@ -6,6 +6,9 @@
/uploads /uploads
/test/uploads /test/uploads
/.elixir_ls /.elixir_ls
/test/fixtures/test_tmp.txt
/test/fixtures/image_tmp.jpg
/doc
# Prevent committing custom emojis # Prevent committing custom emojis
/priv/static/emoji/custom/* /priv/static/emoji/custom/*
@ -28,4 +31,4 @@ erl_crash.dump
.env .env
# Editor config # Editor config
/.vscode /.vscode

View file

@ -10,18 +10,18 @@
config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes
# Upload configuration
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local, uploader: Pleroma.Uploaders.Local,
strip_exif: false filters: [],
proxy_remote: false,
proxy_opts: []
config :pleroma, Pleroma.Uploaders.Local, config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
uploads: "uploads",
uploads_url: "{{base_url}}/media/{{file}}"
config :pleroma, Pleroma.Uploaders.S3, config :pleroma, Pleroma.Uploaders.S3,
bucket: nil, bucket: nil,
public_endpoint: "https://s3.amazonaws.com", public_endpoint: "https://s3.amazonaws.com"
force_media_proxy: false
config :pleroma, Pleroma.Uploaders.MDII, config :pleroma, Pleroma.Uploaders.MDII,
cgi: "https://mdii.sakura.ne.jp/mdii-post.cgi", cgi: "https://mdii.sakura.ne.jp/mdii-post.cgi",
@ -72,6 +72,7 @@
config :pleroma, :websub, Pleroma.Web.Websub config :pleroma, :websub, Pleroma.Web.Websub
config :pleroma, :ostatus, Pleroma.Web.OStatus config :pleroma, :ostatus, Pleroma.Web.OStatus
config :pleroma, :httpoison, Pleroma.HTTP config :pleroma, :httpoison, Pleroma.HTTP
config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc. # Configures http settings, upstream proxy etc.
config :pleroma, :http, proxy_url: nil config :pleroma, :http, proxy_url: nil
@ -150,9 +151,11 @@
config :pleroma, :media_proxy, config :pleroma, :media_proxy,
enabled: false, enabled: false,
redirect_on_failure: true # base_url: "https://cache.pleroma.social",
proxy_opts: [
# base_url: "https://cache.pleroma.social" # inline_content_types: [] | false | true,
# http: [:insecure]
]
config :pleroma, :chat, enabled: true config :pleroma, :chat, enabled: true

View file

@ -5,11 +5,19 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
## Pleroma.Upload ## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use * `uploader`: Select which `Pleroma.Uploaders` to use
* `strip_exif`: boolean, uses ImageMagick(!) to strip exif. * `filters`: List of `Pleroma.Upload.Filter` to use.
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
## Pleroma.Uploaders.Local ## Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleromas working directory * `uploads`: Which directory to store the user-uploads in, relative to pleromas working directory
* `uploads_url`: The URL to access a user-uploaded file, ``{{base_url}}`` is replaced to the instance URL and ``{{file}}`` to the filename. Useful when you want to proxy the media files via another host.
## Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", {"impode", "1"}]`.
## :uri_schemes ## :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL * `valid_schemes`: List of the scheme part that is considered valid to be an URL
@ -68,7 +76,8 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
## :media_proxy ## :media_proxy
* `enabled`: Enables proxying of remote media to the instances proxy * `enabled`: Enables proxying of remote media to the instances proxy
* `redirect_on_failure`: Use the original URL when Media Proxy fails to get it * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
## :gopher ## :gopher
* `enabled`: Enables the gopher interface * `enabled`: Enables the gopher interface

View file

@ -49,11 +49,10 @@
hostname: "localhost", hostname: "localhost",
pool_size: 10 pool_size: 10
try do if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs" import_config "dev.secret.exs"
rescue else
_ -> IO.puts(
IO.puts( "!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs"
"!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs" )
)
end end

View file

@ -9,7 +9,7 @@
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, level: :warn
config :pleroma, Pleroma.Upload, uploads: "test/uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
# Configure your database # Configure your database
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
@ -25,7 +25,7 @@
config :pleroma, :websub, Pleroma.Web.WebsubMock config :pleroma, :websub, Pleroma.Web.WebsubMock
config :pleroma, :ostatus, Pleroma.Web.OStatusMock config :pleroma, :ostatus, Pleroma.Web.OStatusMock
config :pleroma, :httpoison, HTTPoisonMock config :tesla, adapter: Tesla.Mock
try do try do
import_config "test.secret.exs" import_config "test.secret.exs"

View file

@ -70,10 +70,12 @@ server {
client_max_body_size 16m; client_max_body_size 16m;
} }
location /proxy { location ~ ^/(media|proxy) {
proxy_cache pleroma_media_cache; proxy_cache pleroma_media_cache;
proxy_cache_lock on; proxy_cache_lock on;
proxy_ignore_client_abort on; proxy_ignore_client_abort on;
proxy_buffering off;
chunked_transfer_encoding on;
proxy_pass http://localhost:4000; proxy_pass http://localhost:4000;
} }
} }

View file

@ -8,7 +8,7 @@ defmodule Mix.Tasks.SetModerator do
""" """
use Mix.Task use Mix.Task
import Mix.Ecto import Ecto.Changeset
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
def run([nickname | rest]) do def run([nickname | rest]) do
@ -21,14 +21,15 @@ def run([nickname | rest]) do
end end
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_by_nickname(nickname) do
info = info_cng = User.Info.admin_api_update(user.info, %{is_moderator: !!moderator})
user.info
|> Map.put("is_moderator", !!moderator)
cng = User.info_changeset(user, %{info: info}) user_cng =
{:ok, user} = User.update_and_set_cache(cng) Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)
IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}") {:ok, user} = User.update_and_set_cache(user_cng)
IO.puts("Moderator status of #{nickname}: #{user.info.is_moderator}")
else else
_ -> _ ->
IO.puts("No local user #{nickname}") IO.puts("No local user #{nickname}")

View file

@ -0,0 +1,97 @@
defmodule Mix.Tasks.MigrateLocalUploads do
use Mix.Task
import Mix.Ecto
alias Pleroma.{Upload, Uploaders.Local, Uploaders.S3}
require Logger
@log_every 50
@shortdoc "Migrate uploads from local to remote storage"
def run([target_uploader | args]) do
delete? = Enum.member?(args, "--delete")
Application.ensure_all_started(:pleroma)
local_path = Pleroma.Config.get!([Local, :uploads])
uploader = Module.concat(Pleroma.Uploaders, target_uploader)
unless Code.ensure_loaded?(uploader) do
raise("The uploader #{inspect(uploader)} is not an existing/loaded module.")
end
target_enabled? = Pleroma.Config.get([Upload, :uploader]) == uploader
unless target_enabled? do
Pleroma.Config.put([Upload, :uploader], uploader)
end
Logger.info("Migrating files from local #{local_path} to #{to_string(uploader)}")
if delete? do
Logger.warn(
"Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"
)
:timer.sleep(:timer.seconds(5))
end
uploads =
File.ls!(local_path)
|> Enum.map(fn id ->
root_path = Path.join(local_path, id)
cond do
File.dir?(root_path) ->
files = for file <- File.ls!(root_path), do: {id, file, Path.join([root_path, file])}
case List.first(files) do
{id, file, path} ->
{%Pleroma.Upload{id: id, name: file, path: id <> "/" <> file, tempfile: path},
root_path}
_ ->
nil
end
File.exists?(root_path) ->
file = Path.basename(id)
[hash, ext] = String.split(id, ".")
{%Pleroma.Upload{id: hash, name: file, path: file, tempfile: root_path}, root_path}
true ->
nil
end
end)
|> Enum.filter(& &1)
total_count = length(uploads)
Logger.info("Found #{total_count} uploads")
uploads
|> Task.async_stream(
fn {upload, root_path} ->
case Upload.store(upload, uploader: uploader, filters: [], size_limit: nil) do
{:ok, _} ->
if delete?, do: File.rm_rf!(root_path)
Logger.debug("uploaded: #{inspect(upload.path)} #{inspect(upload)}")
:ok
error ->
Logger.error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
end
end,
timeout: 150_000
)
|> Stream.chunk_every(@log_every)
|> Enum.reduce(0, fn done, count ->
count = count + length(done)
Logger.info("Uploaded #{count}/#{total_count} files")
count
end)
Logger.info("Done!")
end
def run(_) do
Logger.error("Usage: migrate_local_uploads S3|Swift [--delete]")
end
end

View file

@ -4,3 +4,4 @@ CREATE DATABASE pleroma_dev OWNER pleroma;
--Extensions made by ecto.migrate that need superuser access --Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

View file

@ -1,5 +1,6 @@
defmodule Mix.Tasks.SetAdmin do defmodule Mix.Tasks.SetAdmin do
use Mix.Task use Mix.Task
import Ecto.Changeset
alias Pleroma.User alias Pleroma.User
@doc """ @doc """
@ -9,21 +10,22 @@ defmodule Mix.Tasks.SetAdmin do
def run([nickname | rest]) do def run([nickname | rest]) do
Application.ensure_all_started(:pleroma) Application.ensure_all_started(:pleroma)
status = admin =
case rest do case rest do
[status] -> status == "true" [admin] -> admin == "true"
_ -> true _ -> true
end end
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_by_nickname(nickname) do
info = info_cng = User.Info.admin_api_update(user.info, %{is_admin: !!admin})
user.info
|> Map.put("is_admin", !!status)
cng = User.info_changeset(user, %{info: info}) user_cng =
{:ok, user} = User.update_and_set_cache(cng) Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)
IO.puts("Admin status of #{nickname}: #{user.info["is_admin"]}") {:ok, user} = User.update_and_set_cache(user_cng)
IO.puts("Admin status of #{nickname}: #{user.info.is_admin}")
else else
_ -> _ ->
IO.puts("No local user #{nickname}") IO.puts("No local user #{nickname}")

View file

@ -10,11 +10,11 @@ defmodule Mix.Tasks.SetLocked do
""" """
use Mix.Task use Mix.Task
import Mix.Ecto import Ecto.Changeset
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
def run([nickname | rest]) do def run([nickname | rest]) do
ensure_started(Repo, []) Application.ensure_all_started(:pleroma)
locked = locked =
case rest do case rest do
@ -23,14 +23,15 @@ def run([nickname | rest]) do
end end
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_by_nickname(nickname) do
info = info_cng = User.Info.profile_update(user.info, %{locked: !!locked})
user.info
|> Map.put("locked", !!locked)
cng = User.info_changeset(user, %{info: info}) user_cng =
user = Repo.update!(cng) Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)
IO.puts("locked status of #{nickname}: #{user.info["locked"]}") {:ok, user} = User.update_and_set_cache(user_cng)
IO.puts("Locked status of #{nickname}: #{user.info.locked}")
else else
_ -> _ ->
IO.puts("No local user #{nickname}") IO.puts("No local user #{nickname}")

View file

@ -1,5 +1,6 @@
defmodule Pleroma.Application do defmodule Pleroma.Application do
use Application use Application
import Supervisor.Spec
@name "Pleroma" @name "Pleroma"
@version Mix.Project.config()[:version] @version Mix.Project.config()[:version]
@ -7,11 +8,15 @@ def name, do: @name
def version, do: @version def version, do: @version
def named_version(), do: @name <> " " <> @version def named_version(), do: @name <> " " <> @version
def user_agent() do
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
end
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
@env Mix.env() @env Mix.env()
def start(_type, _args) do def start(_type, _args) do
import Supervisor.Spec
import Cachex.Spec import Cachex.Spec
# Define workers and child supervisors to be supervised # Define workers and child supervisors to be supervised
@ -20,10 +25,6 @@ def start(_type, _args) do
# Start the Ecto repository # Start the Ecto repository
supervisor(Pleroma.Repo, []), supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []), worker(Pleroma.Emoji, []),
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker( worker(
Cachex, Cachex,
[ [
@ -63,21 +64,18 @@ def start(_type, _args) do
], ],
id: :cachex_idem id: :cachex_idem
), ),
worker(Pleroma.Web.Federator, []),
worker(Pleroma.Web.Federator.RetryQueue, []), worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Gopher.Server, []), worker(Pleroma.Web.Federator, []),
worker(Pleroma.Stats, []), worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, []) worker(Pleroma.Web.Push, [])
] ++ ] ++
if @env == :test, streamer_child() ++
do: [], chat_child() ++
else: [
[worker(Pleroma.Web.Streamer, [])] ++ # Start the endpoint when the application starts
if( supervisor(Pleroma.Web.Endpoint, []),
!chat_enabled(), worker(Pleroma.Gopher.Server, [])
do: [], ]
else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
)
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options
@ -85,7 +83,20 @@ def start(_type, _args) do
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
defp chat_enabled do if Mix.env() == :test do
Application.get_env(:pleroma, :chat, []) |> Keyword.get(:enabled) defp streamer_child(), do: []
defp chat_child(), do: []
else
defp streamer_child() do
[worker(Pleroma.Web.Streamer, [])]
end
defp chat_child() do
if Pleroma.Config.get([:chat, :enabled]) do
[worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
else
[]
end
end
end end
end end

View file

@ -39,4 +39,18 @@ def put([parent_key | keys], value) do
def put(key, value) do def put(key, value) do
Application.put_env(:pleroma, key, value) Application.put_env(:pleroma, key, value)
end end
def delete([key]), do: delete(key)
def delete([parent_key | keys]) do
{_, parent} =
Application.get_env(:pleroma, parent_key)
|> get_and_update_in(keys, fn _ -> :pop end)
Application.put_env(:pleroma, parent_key, parent)
end
def delete(key) do
Application.delete_env(:pleroma, key)
end
end end

View file

@ -114,10 +114,10 @@ def add_user_links({subs, text}, mentions) do
subs = subs =
subs ++ subs ++
Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} -> Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
ap_id = ap_id =
if is_binary(info["source_data"]["url"]) do if is_binary(info.source_data["url"]) do
info["source_data"]["url"] info.source_data["url"]
else else
ap_id ap_id
end end
@ -125,7 +125,7 @@ def add_user_links({subs, text}, mentions) do
short_match = String.split(match, "@") |> tl() |> hd() short_match = String.split(match, "@") |> tl() |> hd()
{uuid, {uuid,
"<span><a class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"} "<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
end) end)
{subs, uuid_text} {subs, uuid_text}
@ -147,7 +147,11 @@ def add_hashtag_links({subs, text}, tags) do
subs = subs =
subs ++ subs ++
Enum.map(tags, fn {tag_text, tag, uuid} -> Enum.map(tags, fn {tag_text, tag, uuid} ->
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>" url =
"<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
tag_text
}</a>"
{uuid, url} {uuid, url}
end) end)

View file

@ -6,27 +6,28 @@ def start_link() do
config = Pleroma.Config.get(:gopher, []) config = Pleroma.Config.get(:gopher, [])
ip = Keyword.get(config, :ip, {0, 0, 0, 0}) ip = Keyword.get(config, :ip, {0, 0, 0, 0})
port = Keyword.get(config, :port, 1234) port = Keyword.get(config, :port, 1234)
GenServer.start_link(__MODULE__, [ip, port], [])
if Keyword.get(config, :enabled, false) do
GenServer.start_link(__MODULE__, [ip, port], [])
else
Logger.info("Gopher server disabled")
:ignore
end
end end
def init([ip, port]) do def init([ip, port]) do
if Pleroma.Config.get([:gopher, :enabled], false) do Logger.info("Starting gopher server on #{port}")
Logger.info("Starting gopher server on #{port}")
:ranch.start_listener( :ranch.start_listener(
:gopher, :gopher,
100, 100,
:ranch_tcp, :ranch_tcp,
[port: port], [port: port],
__MODULE__.ProtocolHandler, __MODULE__.ProtocolHandler,
[] []
) )
{:ok, %{ip: ip, port: port}} {:ok, %{ip: ip, port: port}}
else
Logger.info("Gopher server disabled")
{:ok, nil}
end
end end
end end

View file

@ -45,7 +45,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.strip_comments() Meta.strip_comments()
# links # links
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"]) Meta.allow_tag_with_these_attributes("a", ["name", "title"])
# paragraphs and linebreaks # paragraphs and linebreaks
@ -86,7 +86,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.remove_cdata_sections_before_scrub() Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments() Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"]) Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"]) Meta.allow_tag_with_these_attributes("abbr", ["title"])

View file

@ -0,0 +1,27 @@
defmodule Pleroma.HTTP.Connection do
@moduledoc """
Connection for http-requests.
"""
@hackney_options [pool: :default]
@adapter Application.get_env(:tesla, :adapter)
@doc """
Configure a client connection
# Returns
Tesla.Env.client
"""
@spec new(Keyword.t()) :: Tesla.Env.client()
def new(opts \\ []) do
Tesla.client([], {@adapter, hackney_options(opts)})
end
# fetch Hackney options
#
defp hackney_options(opts \\ []) do
options = Keyword.get(opts, :adapter, [])
@hackney_options ++ options
end
end

View file

@ -1,14 +1,42 @@
defmodule Pleroma.HTTP do defmodule Pleroma.HTTP do
require HTTPoison @moduledoc """
"""
alias Pleroma.HTTP.Connection
alias Pleroma.HTTP.RequestBuilder, as: Builder
@doc """
Builds and perform http request.
# Arguments:
`method` - :get, :post, :put, :delete
`url`
`body`
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
`options` - custom, per-request middleware or adapter options
# Returns:
`{:ok, %Tesla.Env{}}` or `{:error, error}`
"""
def request(method, url, body \\ "", headers \\ [], options \\ []) do def request(method, url, body \\ "", headers \\ [], options \\ []) do
options = options =
process_request_options(options) process_request_options(options)
|> process_sni_options(url) |> process_sni_options(url)
HTTPoison.request(method, url, body, headers, options) %{}
|> Builder.method(method)
|> Builder.headers(headers)
|> Builder.opts(options)
|> Builder.url(url)
|> Builder.add_param(:body, :body, body)
|> Enum.into([])
|> (&Tesla.request(Connection.new(), &1)).()
end end
defp process_sni_options(options, nil), do: options
defp process_sni_options(options, url) do defp process_sni_options(options, url) do
uri = URI.parse(url) uri = URI.parse(url)
host = uri.host |> to_charlist() host = uri.host |> to_charlist()
@ -22,7 +50,7 @@ defp process_sni_options(options, url) do
def process_request_options(options) do def process_request_options(options) do
config = Application.get_env(:pleroma, :http, []) config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil) proxy = Keyword.get(config, :proxy_url, nil)
options = options ++ [hackney: [pool: :default]] options = options ++ [adapter: [pool: :default]]
case proxy do case proxy do
nil -> options nil -> options
@ -30,8 +58,19 @@ def process_request_options(options) do
end end
end end
def get(url, headers \\ [], options \\ []), do: request(:get, url, "", headers, options) @doc """
Performs GET request.
See `Pleroma.HTTP.request/5`
"""
def get(url, headers \\ [], options \\ []),
do: request(:get, url, "", headers, options)
@doc """
Performs POST request.
See `Pleroma.HTTP.request/5`
"""
def post(url, body, headers \\ [], options \\ []), def post(url, body, headers \\ [], options \\ []),
do: request(:post, url, body, headers, options) do: request(:post, url, body, headers, options)
end end

View file

@ -0,0 +1,126 @@
defmodule Pleroma.HTTP.RequestBuilder do
@moduledoc """
Helper functions for building Tesla requests
"""
@doc """
Specify the request method when building a request
## Parameters
- request (Map) - Collected request options
- m (atom) - Request method
## Returns
Map
"""
@spec method(map(), atom) :: map()
def method(request, m) do
Map.put_new(request, :method, m)
end
@doc """
Specify the request method when building a request
## Parameters
- request (Map) - Collected request options
- u (String) - Request URL
## Returns
Map
"""
@spec url(map(), String.t()) :: map()
def url(request, u) do
Map.put_new(request, :url, u)
end
@doc """
Add headers to the request
"""
@spec headers(map(), list(tuple)) :: map()
def headers(request, h) do
Map.put_new(request, :headers, h)
end
@doc """
Add custom, per-request middleware or adapter options to the request
"""
@spec opts(map(), Keyword.t()) :: map()
def opts(request, options) do
Map.put_new(request, :opts, options)
end
@doc """
Add optional parameters to the request
## Parameters
- request (Map) - Collected request options
- definitions (Map) - Map of parameter name to parameter location.
- options (KeywordList) - The provided optional parameters
## Returns
Map
"""
@spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map()
def add_optional_params(request, _, []), do: request
def add_optional_params(request, definitions, [{key, value} | tail]) do
case definitions do
%{^key => location} ->
request
|> add_param(location, key, value)
|> add_optional_params(definitions, tail)
_ ->
add_optional_params(request, definitions, tail)
end
end
@doc """
Add optional parameters to the request
## Parameters
- request (Map) - Collected request options
- location (atom) - Where to put the parameter
- key (atom) - The name of the parameter
- value (any) - The value of the parameter
## Returns
Map
"""
@spec add_param(map(), atom, atom, any()) :: map()
def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
def add_param(request, :body, key, value) do
request
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|> Map.update!(
:body,
&Tesla.Multipart.add_field(&1, key, Poison.encode!(value),
headers: [{:"Content-Type", "application/json"}]
)
)
end
def add_param(request, :file, name, path) do
request
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name))
end
def add_param(request, :form, name, value) do
request
|> Map.update(:body, %{name => value}, &Map.put(&1, name, value))
end
def add_param(request, location, key, value) do
Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
end
end

108
lib/pleroma/mime.ex Normal file
View file

@ -0,0 +1,108 @@
defmodule Pleroma.MIME do
@moduledoc """
Returns the mime-type of a binary and optionally a normalized file-name.
"""
@default "application/octet-stream"
@read_bytes 31
@spec file_mime_type(String.t()) ::
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
def file_mime_type(path, filename) do
with {:ok, content_type} <- file_mime_type(path),
filename <- fix_extension(filename, content_type) do
{:ok, content_type, filename}
end
end
@spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error
def file_mime_type(filename) do
File.open(filename, [:read], fn f ->
check_mime_type(IO.binread(f, @read_bytes))
end)
end
def bin_mime_type(binary, filename) do
with {:ok, content_type} <- bin_mime_type(binary),
filename <- fix_extension(filename, content_type) do
{:ok, content_type, filename}
end
end
@spec bin_mime_type(binary()) :: {:ok, String.t()} | :error
def bin_mime_type(<<head::binary-size(@read_bytes), _::binary>>) do
{:ok, check_mime_type(head)}
end
def mime_type(<<_::binary>>), do: {:ok, @default}
def bin_mime_type(_), do: :error
defp fix_extension(filename, content_type) do
parts = String.split(filename, ".")
new_filename =
if length(parts) > 1 do
Enum.drop(parts, -1) |> Enum.join(".")
else
Enum.join(parts)
end
cond do
content_type == "application/octet-stream" ->
filename
ext = List.first(MIME.extensions(content_type)) ->
new_filename <> "." <> ext
true ->
Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".")
end
end
defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do
"image/png"
end
defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do
"image/gif"
end
defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do
"image/jpeg"
end
defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do
"video/webm"
end
defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do
"video/mp4"
end
defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do
"audio/mpeg"
end
defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do
"audio/mpeg"
end
defp check_mime_type(
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65,
0x6F, 0x72, 0x61, _::binary>>
) do
"video/ogg"
end
defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do
"audio/ogg"
end
defp check_mime_type(<<0x52, 0x49, 0x46, 0x46, _::binary>>) do
"audio/wav"
end
defp check_mime_type(_) do
@default
end
end

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Object do defmodule Pleroma.Object do
use Ecto.Schema use Ecto.Schema
alias Pleroma.{Repo, Object, Activity} alias Pleroma.{Repo, Object, User, Activity}
import Ecto.{Query, Changeset} import Ecto.{Query, Changeset}
schema "objects" do schema "objects" do
@ -31,6 +31,13 @@ def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id) def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
def normalize(_), do: nil 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}),
do: actor == ap_id
# Legacy objects can be mutated by anybody
def authorize_mutation(%Object{}, %User{}), do: true
if Mix.env() == :test do if Mix.env() == :test do
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
get_by_ap_id(ap_id) get_by_ap_id(ap_id)

View file

@ -1,26 +1,22 @@
defmodule Pleroma.Plugs.OAuthPlug do defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn import Plug.Conn
alias Pleroma.User import Ecto.Query
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Token
def init(options) do alias Pleroma.{
options User,
end Repo,
Web.OAuth.Token
}
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do def call(conn, _) do
token = with {:ok, token} <- fetch_token(conn),
case get_req_header(conn, "authorization") do {:ok, user} <- fetch_user(token) do
["Bearer " <> header] -> header
_ -> get_session(conn, :oauth_token)
end
with token when not is_nil(token) <- token,
%Token{user_id: user_id} = token <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id),
false <- !!user.info["deactivated"] do
conn conn
|> assign(:token, token) |> assign(:token, token)
|> assign(:user, user) |> assign(:user, user)
@ -28,4 +24,47 @@ def call(conn, _) do
_ -> conn _ -> conn
end end
end end
# Gets user by token
#
@spec fetch_user(String.t()) :: {:ok, User.t()} | nil
defp fetch_user(token) do
query = from(q in Token, where: q.token == ^token, preload: [:user])
with %Token{user: %{info: %{deactivated: false} = _} = user} <- Repo.one(query) do
{:ok, user}
end
end
# Gets token from session by :oauth_token key
#
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_from_session(conn) do
case get_session(conn, :oauth_token) do
nil -> :no_token_found
token -> {:ok, token}
end
end
# Gets token from headers
#
@spec fetch_token(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token(%Plug.Conn{} = conn) do
headers = get_req_header(conn, "authorization")
with :no_token_found <- fetch_token(headers),
do: fetch_token_from_session(conn)
end
@spec fetch_token(Keyword.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token([]), do: :no_token_found
defp fetch_token([token | tail]) do
trimmed_token = String.trim(token)
case Regex.run(@realm_reg, trimmed_token) do
[_, match] -> {:ok, String.trim(match)}
_ -> fetch_token(tail)
end
end
end end

View file

@ -0,0 +1,78 @@
defmodule Pleroma.Plugs.UploadedMedia do
@moduledoc """
"""
import Plug.Conn
require Logger
@behaviour Plug
# no slashes
@path "media"
@cache_control %{
default: "public, max-age=1209600",
error: "public, must-revalidate, max-age=160"
}
def init(_opts) do
static_plug_opts =
[]
|> Keyword.put(:from, "__unconfigured_media_plug")
|> Keyword.put(:at, "/__unconfigured_media_plug")
|> Plug.Static.init()
%{static_plug_opts: static_plug_opts}
end
def call(conn = %{request_path: <<"/", @path, "/", file::binary>>}, opts) do
config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
{:ok, get_method} <- uploader.get_file(file) do
get_media(conn, get_method, proxy_remote, opts)
else
_ ->
conn
|> send_resp(500, "Failed")
|> halt()
end
end
def call(conn, _opts), do: conn
defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts =
Map.get(opts, :static_plug_opts)
|> Map.put(:at, [@path])
|> Map.put(:from, directory)
conn = Plug.Static.call(conn, static_opts)
if conn.halted do
conn
else
conn
|> send_resp(404, "Not found")
|> halt()
end
end
defp get_media(conn, {:url, url}, true, _) do
conn
|> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], []))
end
defp get_media(conn, {:url, url}, _, _) do
conn
|> Phoenix.Controller.redirect(external: url)
|> halt()
end
defp get_media(conn, unknown, _, _) do
Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
conn
|> send_resp(500, "Internal Error")
|> halt()
end
end

View file

@ -6,7 +6,7 @@ def init(options) do
options options
end end
def call(%{assigns: %{user: %User{info: %{"deactivated" => true}}}} = conn, _) do def call(%{assigns: %{user: %User{info: %{deactivated: true}}}} = conn, _) do
conn conn
|> assign(:user, nil) |> assign(:user, nil)
end end

View file

@ -6,7 +6,7 @@ def init(options) do
options options
end end
def call(%{assigns: %{user: %User{info: %{"is_admin" => true}}}} = conn, _) do def call(%{assigns: %{user: %User{info: %{is_admin: true}}}} = conn, _) do
conn conn
end end

View file

@ -0,0 +1,343 @@
defmodule Pleroma.ReverseProxy do
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control)
@keep_resp_headers @resp_cache_headers ++
~w(content-type content-disposition content-encoding content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity
@methods ~w(GET HEAD)
@moduledoc """
A reverse proxy.
Pleroma.ReverseProxy.call(conn, url, options)
It is not meant to be added into a plug pipeline, but to be called from another plug or controller.
Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes.
Responses are chunked to the client while downloading from the upstream.
Some request / responses headers are preserved:
* request: `#{inspect(@keep_req_headers)}`
* response: `#{inspect(@keep_resp_headers)}`
If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be
set to `#{inspect(@default_cache_control_header)}`.
Options:
* `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP
errors. Any error during body processing will not be redirected as the response is chunked. This may expose
remote URL, clients IPs, .
* `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the
specified length. It is validated with the `content-length` header and also verified when proxying.
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
read from the remote upstream.
* `inline_content_types`:
* `true` will not alter `content-disposition` (up to the upstream),
* `false` will add `content-disposition: attachment` to any request,
* a list of whitelisted content types
* `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is
doing content transformation (encoding, ) depending on the request.
* `req_headers`, `resp_headers` additional headers.
* `http`: options for [hackney](https://github.com/benoitc/hackney).
"""
@hackney Application.get_env(:pleroma, :hackney, :hackney)
@httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
@default_hackney_options [{:follow_redirect, true}]
@inline_content_types [
"image/gif",
"image/jpeg",
"image/jpg",
"image/png",
"image/svg+xml",
"audio/mpeg",
"audio/mp3",
"video/webm",
"video/mp4",
"video/quicktime"
]
require Logger
import Plug.Conn
@type option() ::
{:keep_user_agent, boolean}
| {:max_read_duration, :timer.time() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
| {:http, []}
| {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]}
| {:inline_content_types, boolean() | [String.t()]}
| {:redirect_on_failure, boolean()}
@spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
def call(conn = %{method: method}, url, opts \\ []) when method in @methods do
hackney_opts =
@default_hackney_options
|> Keyword.merge(Keyword.get(opts, :http, []))
|> @httpoison.process_request_options()
req_headers = build_req_headers(conn.req_headers, opts)
opts =
if filename = Pleroma.Web.MediaProxy.filename(url) do
Keyword.put_new(opts, :attachment_name, filename)
else
opts
end
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
:ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do
response(conn, client, url, code, headers, opts)
else
{:ok, code, headers} ->
head_response(conn, url, code, headers, opts)
|> halt()
{:error, {:invalid_http_response, code}} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
conn
|> error_or_redirect(
url,
code,
"Request failed: " <> Plug.Conn.Status.reason_phrase(code),
opts
)
|> halt()
{:error, error} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
conn
|> error_or_redirect(url, 500, "Request failed", opts)
|> halt()
end
end
def call(conn, _, _) do
conn
|> send_resp(400, Plug.Conn.Status.reason_phrase(400))
|> halt()
end
defp request(method, url, headers, hackney_opts) 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
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
{:ok, code, headers} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers)}
{:ok, code, _, _} ->
{:error, {:invalid_http_response, code}}
{:error, error} ->
{:error, error}
end
end
defp response(conn, client, url, status, headers, opts) do
result =
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_chunked(status)
|> chunk_reply(client, opts)
case result do
{:ok, conn} ->
halt(conn)
{:error, :closed, conn} ->
:hackney.close(client)
halt(conn)
{:error, error, conn} ->
Logger.warn(
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
)
:hackney.close(client)
halt(conn)
end
end
defp chunk_reply(conn, client, opts) do
chunk_reply(conn, client, opts, 0, 0)
end
defp chunk_reply(conn, client, opts, sent_so_far, duration) do
with {:ok, duration} <-
check_read_duration(
duration,
Keyword.get(opts, :max_read_duration, @max_read_duration)
),
{:ok, data} <- @hackney.stream_body(client),
{:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
{:ok, conn} <- chunk(conn, data) do
chunk_reply(conn, client, opts, sent_so_far, duration)
else
:done -> {:ok, conn}
{:error, error} -> {:error, error, conn}
end
end
defp head_response(conn, _url, code, headers, opts) do
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_resp(code, "")
end
defp error_or_redirect(conn, url, code, body, opts) do
if Keyword.get(opts, :redirect_on_failure, false) do
conn
|> Phoenix.Controller.redirect(external: url)
|> halt()
else
conn
|> send_resp(code, body)
|> halt
end
end
defp downcase_headers(headers) do
Enum.map(headers, fn {k, v} ->
{String.downcase(k), v}
end)
end
defp get_content_type(headers) do
{_, content_type} =
List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
[content_type | _] = String.split(content_type, ";")
content_type
end
defp put_resp_headers(conn, headers) do
Enum.reduce(headers, conn, fn {k, v}, conn ->
put_resp_header(conn, k, v)
end)
end
defp build_req_headers(headers, opts) do
headers =
headers
|> downcase_headers()
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|> (fn headers ->
headers = headers ++ Keyword.get(opts, :req_headers, [])
if Keyword.get(opts, :keep_user_agent, false) do
List.keystore(
headers,
"user-agent",
0,
{"user-agent", Pleroma.Application.user_agent()}
)
else
headers
end
end).()
end
defp build_resp_headers(headers, opts) do
headers
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts)
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
end
defp build_resp_cache_headers(headers, opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
if has_cache? do
headers
else
List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header})
end
end
defp build_resp_content_disposition_header(headers, opts) do
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
content_type = get_content_type(headers)
attachment? =
cond do
is_list(opt) && !Enum.member?(opt, content_type) -> true
opt == false -> true
true -> false
end
if attachment? do
disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
else
headers
end
end
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
with {_, size} <- List.keyfind(headers, "content-length", 0),
{size, _} <- Integer.parse(size),
true <- size <= limit do
:ok
else
false ->
{:error, :body_too_large}
_ ->
:ok
end
end
defp header_length_constraint(_, _), do: :ok
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
{:error, :body_too_large}
end
defp body_size_constraint(_, _), do: :ok
defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do
{:error, :read_duration_exceeded}
else
{:ok, {duration, :erlang.system_time(:millisecond)}}
end
end
defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit}
defp increase_read_duration({previous_duration, started})
when is_integer(previous_duration) and is_integer(started) do
duration = :erlang.system_time(:millisecond) - started
{:ok, previous_duration + duration}
end
defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit}
end
end

View file

@ -1,81 +1,209 @@
defmodule Pleroma.Upload do defmodule Pleroma.Upload do
@moduledoc """
# Upload
Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
* `:description`: upload alternative text
* `:base_url`: override base url
* `:uploader`: override uploader
* `:filters`: override filters
* `:size_limit`: override size limit
* `:activity_type`: override activity type
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
* `:id` - the upload id.
* `:name` - the upload file name.
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
Related behaviors:
* `Pleroma.Uploaders.Uploader`
* `Pleroma.Upload.Filter`
"""
alias Ecto.UUID alias Ecto.UUID
require Logger
def check_file_size(path, nil), do: true @type source ::
Plug.Upload.t() | data_uri_string ::
String.t() | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
def check_file_size(path, size_limit) do @type option ::
{:ok, %{size: size}} = File.stat(path) {:type, :avatar | :banner | :background}
size <= size_limit | {:description, String.t()}
end | {:activity_type, String.t()}
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
def store(file, should_dedupe, size_limit \\ nil) @type t :: %__MODULE__{
id: String.t(),
name: String.t(),
tempfile: String.t(),
content_type: String.t(),
path: String.t()
}
defstruct [:id, :name, :tempfile, :content_type, :path]
def store(%Plug.Upload{} = file, should_dedupe, size_limit) do @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
content_type = get_content_type(file.path) def store(upload, opts \\ []) do
opts = get_opts(opts)
with uuid <- get_uuid(file, should_dedupe), with {:ok, upload} <- prepare_upload(upload, opts),
name <- get_name(file, uuid, content_type, should_dedupe), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
true <- check_file_size(file.path, size_limit) do {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
strip_exif_data(content_type, file.path) {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
{:ok, url_path} = uploader().put_file(name, uuid, file.path, content_type, should_dedupe) %{
"type" => opts.activity_type,
%{ "url" => [
"type" => "Document", %{
"url" => [ "type" => "Link",
%{ "mediaType" => upload.content_type,
"type" => "Link", "href" => url_from_spec(opts.base_url, url_spec)
"mediaType" => content_type, }
"href" => url_path ],
} "name" => Map.get(opts, :description) || upload.name
], }}
"name" => name
}
else else
_e -> nil {:error, error} ->
end Logger.error(
end "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
def store(%{"img" => "data:image/" <> image_data}, should_dedupe, size_limit) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
with tmp_path <- tempfile_for_image(data),
uuid <- UUID.generate(),
true <- check_file_size(tmp_path, size_limit) do
content_type = get_content_type(tmp_path)
strip_exif_data(content_type, tmp_path)
name =
create_name(
String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
parsed["filetype"],
content_type
) )
{:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe) {:error, error}
%{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => content_type,
"href" => url_path
}
],
"name" => name
}
else
_e -> nil
end end
end end
@doc """ defp get_opts(opts) do
Creates a tempfile using the Plug.Upload Genserver which cleans them up {size_limit, activity_type} =
automatically. case Keyword.get(opts, :type) do
""" :banner ->
def tempfile_for_image(data) do {Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
:avatar ->
{Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
:background ->
{Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
_ ->
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
end
opts = %{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
base_url:
Keyword.get(
opts,
:base_url,
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
)
}
# TODO: 1.0+ : remove old config compatibility
opts =
if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
Logger.warn("""
Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
:pleroma, Pleroma.Upload.Filter.Mogrify, args: "strip"
""")
Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: "strip")
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
else
opts
end
opts =
if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
Logger.warn("""
Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
""")
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
else
opts
end
end
defp prepare_upload(%Plug.Upload{} = file, opts) do
with :ok <- check_file_size(file.path, opts.size_limit),
{:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: name,
tempfile: file.path,
content_type: content_type
}}
end
end
defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
{:ok, content_type, name} <-
Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: name,
tempfile: tmp_path,
content_type: content_type
}}
end
end
# For Mix.Tasks.MigrateLocalUploads
defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do
with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end
end
defp check_binary_size(binary, size_limit)
when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
{:error, :file_too_large}
end
defp check_binary_size(_, _), do: :ok
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
with {:ok, %{size: size}} <- File.stat(path),
true <- size <= size_limit do
:ok
else
false -> {:error, :file_too_large}
error -> error
end
end
defp check_file_size(_, _), do: :ok
# Creates a tempfile using the Plug.Upload Genserver which cleans them up
# automatically.
defp tempfile_for_image(data) do
{:ok, tmp_path} = Plug.Upload.random_file("profile_pics") {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
{:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary]) {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
IO.binwrite(tmp_file, data) IO.binwrite(tmp_file, data)
@ -83,108 +211,12 @@ def tempfile_for_image(data) do
tmp_path tmp_path
end end
def strip_exif_data(content_type, file) do defp url_from_spec(base_url, {:file, path}) do
settings = Application.get_env(:pleroma, Pleroma.Upload) [base_url, "media", path]
do_strip = Keyword.fetch!(settings, :strip_exif) |> Path.join()
[filetype, _ext] = String.split(content_type, "/")
if filetype == "image" and do_strip == true do
Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
end
end end
defp create_name(uuid, ext, type) do defp url_from_spec({:url, url}) do
case type do url
"application/octet-stream" ->
String.downcase(Enum.join([uuid, ext], "."))
"audio/mpeg" ->
String.downcase(Enum.join([uuid, "mp3"], "."))
_ ->
String.downcase(Enum.join([uuid, List.last(String.split(type, "/"))], "."))
end
end
defp get_uuid(file, should_dedupe) do
if should_dedupe do
Base.encode16(:crypto.hash(:sha256, File.read!(file.path)))
else
UUID.generate()
end
end
defp get_name(file, uuid, type, should_dedupe) do
if should_dedupe do
create_name(uuid, List.last(String.split(file.filename, ".")), type)
else
parts = String.split(file.filename, ".")
new_filename =
if length(parts) > 1 do
Enum.drop(parts, -1) |> Enum.join(".")
else
Enum.join(parts)
end
case type do
"application/octet-stream" -> file.filename
"audio/mpeg" -> new_filename <> ".mp3"
"image/jpeg" -> new_filename <> ".jpg"
_ -> Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
end
end
end
def get_content_type(file) do
match =
File.open(file, [:read], fn f ->
case IO.binread(f, 8) do
<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> ->
"image/png"
<<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
"image/gif"
<<0xFF, 0xD8, 0xFF, _, _, _, _, _>> ->
"image/jpeg"
<<0x1A, 0x45, 0xDF, 0xA3, _, _, _, _>> ->
"video/webm"
<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
"video/mp4"
<<0x49, 0x44, 0x33, _, _, _, _, _>> ->
"audio/mpeg"
<<255, 251, _, 68, 0, 0, 0, 0>> ->
"audio/mpeg"
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
case IO.binread(f, 27) do
<<_::size(160), 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61>> ->
"video/ogg"
_ ->
"audio/ogg"
end
<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
"audio/wav"
_ ->
"application/octet-stream"
end
end)
case match do
{:ok, type} -> type
_e -> "application/octet-stream"
end
end
defp uploader() do
Pleroma.Config.get!([Pleroma.Upload, :uploader])
end end
end end

View file

@ -0,0 +1,35 @@
defmodule Pleroma.Upload.Filter do
@moduledoc """
Upload Filter behaviour
This behaviour allows to run filtering actions just before a file is uploaded. This allows to:
* morph in place the temporary file
* change any field of a `Pleroma.Upload` struct
* cancel/stop the upload
"""
require Logger
@callback filter(Pleroma.Upload.t()) :: :ok | {:ok, Pleroma.Upload.t()} | {:error, any()}
@spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.Upload.t()} | {:error, any()}
def filter([], upload) do
{:ok, upload}
end
def filter([filter | rest], upload) do
case filter.filter(upload) do
:ok ->
filter(rest, upload)
{:ok, upload} ->
filter(rest, upload)
error ->
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
error
end
end
end

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Upload.Filter.AnonymizeFilename do
@moduledoc "Replaces the original filename with a randomly generated string."
@behaviour Pleroma.Upload.Filter
def filter(upload) do
extension = List.last(String.split(upload.name, "."))
string = Base.url_encode64(:crypto.strong_rand_bytes(10), padding: false)
{:ok, %Pleroma.Upload{upload | name: string <> "." <> extension}}
end
end

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Upload.Filter.Dedupe do
@behaviour Pleroma.Upload.Filter
def filter(upload = %Pleroma.Upload{name: name, tempfile: path}) do
extension = String.split(name, ".") |> List.last()
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
filename = shasum <> "." <> extension
{:ok, %Pleroma.Upload{upload | id: shasum, path: filename}}
end
end

View file

@ -0,0 +1,60 @@
defmodule Pleroma.Upload.Filter.Mogrifun do
@behaviour Pleroma.Upload.Filter
@filters [
{"implode", "1"},
{"-raise", "20"},
{"+raise", "20"},
[{"-interpolate", "nearest"}, {"-virtual-pixel", "mirror"}, {"-spread", "5"}],
"+polaroid",
{"-statistic", "Mode 10"},
{"-emboss", "0x1.1"},
{"-emboss", "0x2"},
{"-colorspace", "Gray"},
"-negate",
[{"-channel", "green"}, "-negate"],
[{"-channel", "red"}, "-negate"],
[{"-channel", "blue"}, "-negate"],
{"+level-colors", "green,gold"},
{"+level-colors", ",DodgerBlue"},
{"+level-colors", ",Gold"},
{"+level-colors", ",Lime"},
{"+level-colors", ",Red"},
{"+level-colors", ",DarkGreen"},
{"+level-colors", "firebrick,yellow"},
{"+level-colors", "'rgb(102,75,25)',lemonchiffon"},
[{"fill", "red"}, {"tint", "40"}],
[{"fill", "green"}, {"tint", "40"}],
[{"fill", "blue"}, {"tint", "40"}],
[{"fill", "yellow"}, {"tint", "40"}]
]
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
filter = Enum.random(@filters)
file
|> Mogrify.open()
|> mogrify_filter(filter)
|> Mogrify.save(in_place: true)
:ok
end
def filter(_), do: :ok
defp mogrify_filter(mogrify, [filter | rest]) do
mogrify
|> mogrify_filter(filter)
|> mogrify_filter(rest)
end
defp mogrify_filter(mogrify, []), do: mogrify
defp mogrify_filter(mogrify, {action, options}) do
Mogrify.custom(mogrify, action, options)
end
defp mogrify_filter(mogrify, string) when is_binary(string) do
Mogrify.custom(mogrify, string)
end
end

View file

@ -0,0 +1,37 @@
defmodule Pleroma.Upload.Filter.Mogrify do
@behaviour Pleroma.Uploader.Filter
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
filters = Pleroma.Config.get!([__MODULE__, :args])
file
|> Mogrify.open()
|> mogrify_filter(filters)
|> Mogrify.save(in_place: true)
:ok
end
def filter(_), do: :ok
defp mogrify_filter(mogrify, nil), do: mogrify
defp mogrify_filter(mogrify, [filter | rest]) do
mogrify
|> mogrify_filter(filter)
|> mogrify_filter(rest)
end
defp mogrify_filter(mogrify, []), do: mogrify
defp mogrify_filter(mogrify, {action, options}) do
Mogrify.custom(mogrify, action, options)
end
defp mogrify_filter(mogrify, action) when is_binary(action) do
Mogrify.custom(mogrify, action)
end
end

View file

@ -3,49 +3,32 @@ defmodule Pleroma.Uploaders.Local do
alias Pleroma.Web alias Pleroma.Web
def put_file(name, uuid, tmpfile, _content_type, should_dedupe) do def get_file(_) do
upload_folder = get_upload_path(uuid, should_dedupe) {:ok, {:static_dir, upload_path()}}
url_path = get_url(name, uuid, should_dedupe) end
File.mkdir_p!(upload_folder) def put_file(upload) do
{local_path, file} =
case Enum.reverse(String.split(upload.path, "/", trim: true)) do
[file] ->
{upload_path(), file}
result_file = Path.join(upload_folder, name) [file | folders] ->
path = Path.join([upload_path()] ++ Enum.reverse(folders))
File.mkdir_p!(path)
{path, file}
end
if File.exists?(result_file) do result_file = Path.join(local_path, file)
File.rm!(tmpfile)
else unless File.exists?(result_file) do
File.cp!(tmpfile, result_file) File.cp!(upload.tempfile, result_file)
end end
{:ok, url_path} :ok
end end
def upload_path do def upload_path do
settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local) Pleroma.Config.get!([__MODULE__, :uploads])
Keyword.fetch!(settings, :uploads)
end
defp get_upload_path(uuid, should_dedupe) do
if should_dedupe do
upload_path()
else
Path.join(upload_path(), uuid)
end
end
defp get_url(name, uuid, should_dedupe) do
if should_dedupe do
url_for(:cow_uri.urlencode(name))
else
url_for(Path.join(uuid, :cow_uri.urlencode(name)))
end
end
defp url_for(file) do
settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local)
Keyword.get(settings, :uploads_url)
|> String.replace("{{file}}", file)
|> String.replace("{{base_url}}", Web.base_url())
end end
end end

View file

@ -5,22 +5,27 @@ defmodule Pleroma.Uploaders.MDII do
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
def put_file(name, uuid, path, content_type, should_dedupe) do # 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 = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi]) cgi = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi])
files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files]) files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files])
{:ok, file_data} = File.read(path) {:ok, file_data} = File.read(upload.tempfile)
extension = String.split(name, ".") |> List.last() extension = String.split(upload.name, ".") |> List.last()
query = "#{cgi}?#{extension}" query = "#{cgi}?#{extension}"
with {:ok, %{status_code: 200, body: body}} <- @httpoison.post(query, file_data) do with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do
File.rm!(path)
remote_file_name = String.split(body) |> List.first() remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}" public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, public_url} {:ok, {:url, public_url}}
else else
_ -> Pleroma.Uploaders.Local.put_file(name, uuid, path, content_type, should_dedupe) _ -> Pleroma.Uploaders.Local.put_file(upload)
end end
end end
end end

View file

@ -1,40 +1,46 @@
defmodule Pleroma.Uploaders.S3 do defmodule Pleroma.Uploaders.S3 do
alias Pleroma.Web.MediaProxy
@behaviour Pleroma.Uploaders.Uploader @behaviour Pleroma.Uploaders.Uploader
require Logger
def put_file(name, uuid, path, content_type, _should_dedupe) do # The file name is re-encoded with S3's constraints here to comply with previous links with less strict filenames
settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3) def get_file(file) do
bucket = Keyword.fetch!(settings, :bucket) config = Pleroma.Config.get([__MODULE__])
public_endpoint = Keyword.fetch!(settings, :public_endpoint)
force_media_proxy = Keyword.fetch!(settings, :force_media_proxy)
{:ok, file_data} = File.read(path) {:ok,
{:url,
Path.join([
Keyword.fetch!(config, :public_endpoint),
Keyword.fetch!(config, :bucket),
strict_encode(URI.decode(file))
])}}
end
File.rm!(path) def put_file(upload = %Pleroma.Upload{}) do
config = Pleroma.Config.get([__MODULE__])
bucket = Keyword.get(config, :bucket)
s3_name = "#{uuid}/#{encode(name)}" {:ok, file_data} = File.read(upload.tempfile)
{:ok, _} = s3_name = strict_encode(upload.path)
op =
ExAws.S3.put_object(bucket, s3_name, file_data, [ ExAws.S3.put_object(bucket, s3_name, file_data, [
{:acl, :public_read}, {:acl, :public_read},
{:content_type, content_type} {:content_type, upload.content_type}
]) ])
|> ExAws.request()
url_base = "#{public_endpoint}/#{bucket}/#{s3_name}" case ExAws.request(op) do
{:ok, _} ->
{:ok, {:file, s3_name}}
public_url = error ->
if force_media_proxy do Logger.error("#{__MODULE__}: #{inspect(error)}")
MediaProxy.url(url_base) {:error, "S3 Upload failed"}
else end
url_base
end
{:ok, public_url}
end end
defp encode(name) do @regex Regex.compile!("[^0-9a-zA-Z!.*/'()_-]")
String.replace(name, ~r/[^0-9a-zA-Z!.*'()_-]/, "-") def strict_encode(name) do
String.replace(name, @regex, "-")
end end
end end

View file

@ -25,10 +25,10 @@ def get_token() do
["Content-Type": "application/json"], ["Content-Type": "application/json"],
hackney: [:insecure] hackney: [:insecure]
) do ) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} -> {:ok, %Tesla.Env{status: 200, body: body}} ->
body["access"]["token"]["id"] body["access"]["token"]["id"]
{:ok, %HTTPoison.Response{status_code: _}} -> {:ok, %Tesla.Env{status: _}} ->
"" ""
end end
end end

View file

@ -13,10 +13,10 @@ def upload_file(filename, body, content_type) do
token = Pleroma.Uploaders.Swift.Keystone.get_token() token = Pleroma.Uploaders.Swift.Keystone.get_token()
case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do
{:ok, %HTTPoison.Response{status_code: 201}} -> {:ok, %Tesla.Env{status: 201}} ->
{:ok, "#{object_url}/#{filename}"} {:ok, {:file, filename}}
{:ok, %HTTPoison.Response{status_code: 401}} -> {:ok, %Tesla.Env{status: 401}} ->
{:error, "Unauthorized, Bad Token"} {:error, "Unauthorized, Bad Token"}
{:error, _} -> {:error, _} ->

View file

@ -1,10 +1,15 @@
defmodule Pleroma.Uploaders.Swift do defmodule Pleroma.Uploaders.Swift do
@behaviour Pleroma.Uploaders.Uploader @behaviour Pleroma.Uploaders.Uploader
def put_file(name, uuid, tmp_path, content_type, _should_dedupe) do def get_file(name) do
{:ok, file_data} = File.read(tmp_path) {:ok, {:url, Path.join([Pleroma.Config.get!([__MODULE__, :object_url]), name])}}
remote_name = "#{uuid}/#{name}" end
Pleroma.Uploaders.Swift.Client.upload_file(remote_name, file_data, content_type) def put_file(upload) do
Pleroma.Uploaders.Swift.Client.upload_file(
upload.path,
File.read!(upload.tmpfile),
upload.content_type
)
end end
end end

View file

@ -1,20 +1,40 @@
defmodule Pleroma.Uploaders.Uploader do defmodule Pleroma.Uploaders.Uploader do
@moduledoc """ @moduledoc """
Defines the contract to put an uploaded file to any backend. Defines the contract to put and get an uploaded file to any backend.
""" """
@doc """
Instructs how to get the file from the backend.
Used by `Pleroma.Plugs.UploadedMedia`.
"""
@type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()}
@callback get_file(file :: String.t()) :: {:ok, get_method()}
@doc """ @doc """
Put a file to the backend. Put a file to the backend.
Returns `{:ok, String.t } | {:error, String.t} containing the path of the Returns:
uploaded file, or error information if the file failed to be saved to the
respective backend. * `:ok` which assumes `{:ok, upload.path}`
* `{:ok, spec}` where spec is:
* `{:file, filename :: String.t}` to handle reads with `get_file/1` (recommended)
This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL.
* `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity.
* `{:error, String.t}` error information if the file failed to be saved to the backend.
""" """
@callback put_file( @callback put_file(Pleroma.Upload.t()) ::
name :: String.t(), :ok | {:ok, {:file | :url, String.t()}} | {:error, String.t()}
uuid :: String.t(),
file :: File.t(), @spec put_file(module(), Pleroma.Upload.t()) ::
content_type :: String.t(), {:ok, {:file | :url, String.t()}} | {:error, String.t()}
should_dedupe :: Boolean.t() def put_file(uploader, upload) do
) :: {:ok, String.t()} | {:error, String.t()} case uploader.put_file(upload) do
:ok -> {:ok, {:file, upload.path}}
other -> other
end
end
end end

View file

@ -4,6 +4,8 @@ defmodule Pleroma.User do
import Ecto.{Changeset, Query} import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification} alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.{OStatus, Websub, OAuth} alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub} alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
@ -19,11 +21,11 @@ defmodule Pleroma.User do
field(:ap_id, :string) field(:ap_id, :string)
field(:avatar, :map) field(:avatar, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:info, :map, default: %{})
field(:follower_address, :string) field(:follower_address, :string)
field(:search_distance, :float, virtual: true) field(:search_distance, :float, virtual: true)
field(:last_refreshed_at, :naive_datetime) field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification) has_many(:notifications, Notification)
embeds_one(:info, Pleroma.User.Info)
timestamps() timestamps()
end end
@ -36,13 +38,13 @@ def avatar_url(user) do
end end
def banner_url(user) do def banner_url(user) do
case user.info["banner"] do case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href %{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/banner.png" _ -> "#{Web.base_url()}/images/banner.png"
end end
end end
def profile_url(%User{info: %{"source_data" => %{"url" => url}}}), do: url def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil def profile_url(_), do: nil
@ -61,9 +63,7 @@ def follow_changeset(struct, params \\ %{}) do
end end
def info_changeset(struct, params \\ %{}) do def info_changeset(struct, params \\ %{}) do
struct raise "NOT VALID ANYMORE"
|> cast(params, [:info])
|> validate_required([:info])
end end
def user_info(%User{} = user) do def user_info(%User{} = user) do
@ -71,27 +71,34 @@ def user_info(%User{} = user) do
%{ %{
following_count: length(user.following) - oneself, following_count: length(user.following) - oneself,
note_count: user.info["note_count"] || 0, note_count: user.info.note_count,
follower_count: user.info["follower_count"] || 0, follower_count: user.info.follower_count,
locked: user.info["locked"] || false, locked: user.info.locked,
default_scope: user.info["default_scope"] || "public" default_scope: user.info.default_scope
} }
end end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
def remote_user_creation(params) do def remote_user_creation(params) do
params =
params
|> Map.put(:info, params[:info] || %{})
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
changes = changes =
%User{} %User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar]) |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id]) |> validate_required([:name, :ap_id])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex) |> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000) |> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100) |> validate_length(:name, max: 100)
|> put_change(:local, false) |> put_change(:local, false)
|> put_embed(:info, info_cng)
if changes.valid? do if changes.valid? do
case changes.changes[:info]["source_data"] do case info_cng.changes[:source_data] do
%{"followers" => followers} -> %{"followers" => followers} ->
changes changes
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
@ -109,7 +116,7 @@ def remote_user_creation(params) do
def update_changeset(struct, params \\ %{}) do def update_changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:bio, :name]) |> cast(params, [:bio, :name, :avatar])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000) |> validate_length(:bio, max: 5000)
@ -121,12 +128,17 @@ def upgrade_changeset(struct, params \\ %{}) do
params params
|> Map.put(:last_refreshed_at, NaiveDateTime.utc_now()) |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
info_cng =
struct.info
|> User.Info.user_upgrade(params[:info])
struct struct
|> cast(params, [:bio, :name, :info, :follower_address, :avatar, :last_refreshed_at]) |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000) |> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100) |> validate_length(:name, max: 100)
|> put_embed(:info, info_cng)
end end
def password_update_changeset(struct, params) do def password_update_changeset(struct, params) do
@ -165,6 +177,7 @@ def register_changeset(struct, params \\ %{}) do
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000) |> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100) |> validate_length(:name, min: 1, max: 100)
|> put_change(:info, %Pleroma.User.Info{})
if changeset.valid? do if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
@ -191,7 +204,7 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true def needs_update?(_), do: true
def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{"locked" => true}}) do def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
{:ok, follower} {:ok, follower}
end end
@ -222,7 +235,7 @@ def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
cond do cond do
following?(follower, followed) or info["deactivated"] -> following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."} {:error, "Could not follow user: #{followed.nickname} is already on your list."}
deny_follow_blocked and blocks?(followed, follower) -> deny_follow_blocked and blocks?(followed, follower) ->
@ -274,7 +287,7 @@ def following?(%User{} = follower, %User{} = followed) do
end end
def locked?(%User{} = user) do def locked?(%User{} = user) do
user.info["locked"] || false user.info.locked || false
end end
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
@ -411,22 +424,23 @@ def get_follow_requests(%User{} = user) do
end end
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
note_count = (user.info["note_count"] || 0) + 1 info_cng = User.Info.add_to_note_count(user.info, 1)
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info}) cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cs) update_and_set_cache(cng)
end end
def decrease_note_count(%User{} = user) do def decrease_note_count(%User{} = user) do
note_count = user.info["note_count"] || 0 info_cng = User.Info.add_to_note_count(user.info, -1)
note_count = if note_count <= 0, do: 0, else: note_count - 1
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info}) cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cs) update_and_set_cache(cng)
end end
def update_note_count(%User{} = user) do def update_note_count(%User{} = user) do
@ -439,11 +453,13 @@ def update_note_count(%User{} = user) do
note_count = Repo.one(note_count_query) note_count = Repo.one(note_count_query)
new_info = Map.put(user.info, "note_count", note_count) info_cng = User.Info.set_note_count(user.info, note_count)
cs = info_changeset(user, %{info: new_info}) cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cs) update_and_set_cache(cng)
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
@ -457,11 +473,15 @@ def update_follower_count(%User{} = user) do
follower_count = Repo.one(follower_count_query) follower_count = Repo.one(follower_count_query)
new_info = Map.put(user.info, "follower_count", follower_count) info_cng =
user.info
|> User.Info.set_follower_count(follower_count)
cs = info_changeset(user, %{info: new_info}) cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cs) update_and_set_cache(cng)
end end
def get_users_from_set_query(ap_ids, false) do def get_users_from_set_query(ap_ids, false) do
@ -545,12 +565,15 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
unfollow(blocked, blocker) unfollow(blocked, blocker)
end end
blocks = blocker.info["blocks"] || [] info_cng =
new_blocks = Enum.uniq([ap_id | blocks]) blocker.info
new_info = Map.put(blocker.info, "blocks", new_blocks) |> User.Info.add_to_block(ap_id)
cs = User.info_changeset(blocker, %{info: new_info}) cng =
update_and_set_cache(cs) change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
# helper to handle the block given only an actor's AP id # helper to handle the block given only an actor's AP id
@ -558,18 +581,21 @@ def block(blocker, %{ap_id: ap_id}) do
block(blocker, User.get_by_ap_id(ap_id)) block(blocker, User.get_by_ap_id(ap_id))
end end
def unblock(user, %{ap_id: ap_id}) do def unblock(blocker, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || [] info_cng =
new_blocks = List.delete(blocks, ap_id) blocker.info
new_info = Map.put(user.info, "blocks", new_blocks) |> User.Info.remove_from_block(ap_id)
cs = User.info_changeset(user, %{info: new_info}) cng =
update_and_set_cache(cs) change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def blocks?(user, %{ap_id: ap_id}) do def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || [] blocks = user.info.blocks
domain_blocks = user.info["domain_blocks"] || [] domain_blocks = user.info.domain_blocks
%{host: host} = URI.parse(ap_id) %{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) || Enum.member?(blocks, ap_id) ||
@ -579,21 +605,27 @@ def blocks?(user, %{ap_id: ap_id}) do
end end
def block_domain(user, domain) do def block_domain(user, domain) do
domain_blocks = user.info["domain_blocks"] || [] info_cng =
new_blocks = Enum.uniq([domain | domain_blocks]) user.info
new_info = Map.put(user.info, "domain_blocks", new_blocks) |> User.Info.add_to_domain_block(domain)
cs = User.info_changeset(user, %{info: new_info}) cng =
update_and_set_cache(cs) change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def unblock_domain(user, domain) do def unblock_domain(user, domain) do
blocks = user.info["domain_blocks"] || [] info_cng =
new_blocks = List.delete(blocks, domain) user.info
new_info = Map.put(user.info, "domain_blocks", new_blocks) |> User.Info.remove_from_domain_block(domain)
cs = User.info_changeset(user, %{info: new_info}) cng =
update_and_set_cache(cs) change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def local_user_query() do def local_user_query() do
@ -613,9 +645,13 @@ def moderator_user_query() do
end end
def deactivate(%User{} = user, status \\ true) do def deactivate(%User{} = user, status \\ true) do
new_info = Map.put(user.info, "deactivated", status) info_cng = User.Info.set_activation_status(user.info, status)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs) cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def delete(%User{} = user) do def delete(%User{} = user) do
@ -649,7 +685,7 @@ def delete(%User{} = user) do
{:ok, user} {:ok, user}
end end
def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText
end end
@ -683,7 +719,7 @@ def get_or_create_instance_user do
user user
else else
changes = changes =
%User{} %User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local]) |> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, relay_uri) |> put_change(:ap_id, relay_uri)
|> put_change(:nickname, nil) |> put_change(:nickname, nil)
@ -697,7 +733,7 @@ def get_or_create_instance_user do
# AP style # AP style
def public_key_from_info(%{ def public_key_from_info(%{
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}} source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
}) do }) do
key = key =
:public_key.pem_decode(public_key_pem) :public_key.pem_decode(public_key_pem)
@ -708,7 +744,7 @@ def public_key_from_info(%{
end end
# OStatus Magic Key # OStatus Magic Key
def public_key_from_info(%{"magic_key" => magic_key}) do def public_key_from_info(%{magic_key: magic_key}) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)} {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end end
@ -730,11 +766,12 @@ def insert_or_update_user(data) do
|> Map.put(:name, blank?(data[:name]) || data[:nickname]) |> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data) cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname) Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end end
def ap_enabled?(%User{local: true}), do: true def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{info: info}), do: info["ap_enabled"] def ap_enabled?(%User{info: info}), do: info.ap_enabled
def ap_enabled?(_), do: false def ap_enabled?(_), do: false
def get_or_fetch(uri_or_nickname) do def get_or_fetch(uri_or_nickname) do
@ -768,4 +805,18 @@ def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
:error :error
end end
end end
def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) do
mentions = Formatter.parse_mentions(bio)
tags = Formatter.parse_tags(bio)
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
CommonUtils.format_input(bio, mentions, tags, "text/plain") |> Formatter.emojify(emoji)
end
end end

167
lib/pleroma/user/info.ex Normal file
View file

@ -0,0 +1,167 @@
defmodule Pleroma.User.Info do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
field(:keys, :string, default: nil)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, :string, default: nil)
field(:topic, :string, default: nil)
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
field(:hide_network, :boolean, default: false)
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
# avatar -> Where is this used?
# fqn -> Where is this used?
# host -> Where is this used?
# subject _> Where is this used?
end
def set_activation_status(info, deactivated) do
params = %{deactivated: deactivated}
info
|> cast(params, [:deactivated])
|> validate_required([:deactivated])
end
def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
end
def set_note_count(info, number) do
params = %{note_count: Enum.max([0, number])}
info
|> cast(params, [:note_count])
|> validate_required([:note_count])
end
def set_follower_count(info, number) do
params = %{follower_count: Enum.max([0, number])}
info
|> cast(params, [:follower_count])
|> validate_required([:follower_count])
end
def set_blocks(info, blocks) do
params = %{blocks: blocks}
info
|> cast(params, [:blocks])
|> validate_required([:blocks])
end
def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks]))
end
def remove_from_block(info, blocked) do
set_blocks(info, List.delete(info.blocks, blocked))
end
def set_domain_blocks(info, domain_blocks) do
params = %{domain_blocks: domain_blocks}
info
|> cast(params, [:domain_blocks])
|> validate_required([:domain_blocks])
end
def add_to_domain_block(info, domain_blocked) do
set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks]))
end
def remove_from_domain_block(info, domain_blocked) do
set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked))
end
def set_keys(info, keys) do
params = %{keys: keys}
info
|> cast(params, [:keys])
|> validate_required([:keys])
end
def remote_user_creation(info, params) do
info
|> cast(params, [
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key,
:uri,
:hub,
:topic,
:salmon
])
end
def user_upgrade(info, params) do
info
|> cast(params, [
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key
])
end
def profile_update(info, params) do
info
|> cast(params, [
:locked,
:no_rich_text,
:default_scope,
:banner,
:hide_network,
:background
])
end
def mastodon_profile_update(info, params) do
info
|> cast(params, [
:locked,
:banner
])
end
def set_source_data(info, source_data) do
params = %{source_data: source_data}
info
|> cast(params, [:source_data])
|> validate_required([:source_data])
end
def admin_api_update(info, params) do
info
|> cast(params, [
:is_moderator,
:is_admin
])
end
end

View file

@ -42,7 +42,7 @@ defp get_recipients(data) do
defp check_actor_is_active(actor) do defp check_actor_is_active(actor) do
if not is_nil(actor) do if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor), with user <- User.get_cached_by_ap_id(actor),
false <- !!user.info["deactivated"] do false <- user.info.deactivated do
:ok :ok
else else
_e -> :reject _e -> :reject
@ -509,8 +509,8 @@ defp restrict_recent(query, _) do
end end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info["blocks"] || [] blocks = info.blocks || []
domain_blocks = info["domain_blocks"] || [] domain_blocks = info.domain_blocks || []
from( from(
activity in query, activity in query,
@ -572,11 +572,16 @@ def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
|> Enum.reverse() |> Enum.reverse()
end end
def upload(file, size_limit \\ nil) do def upload(file, opts \\ []) do
with data <- with {:ok, data} <- Upload.store(file, opts) do
Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media], size_limit), obj_data =
false <- is_nil(data) do if opts[:actor] do
Repo.insert(%Object{data: data}) Map.put(data, "actor", opts[:actor])
else
data
end
Repo.insert(%Object{data: obj_data})
end end
end end
@ -678,7 +683,7 @@ def publish(actor, activity) do
remote_inboxes = remote_inboxes =
(Pleroma.Web.Salmon.remote_users(activity) ++ followers) (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end) |> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{"source_data" => data}} -> |> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end) end)
|> Enum.uniq() |> Enum.uniq()
@ -764,7 +769,7 @@ def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching #{id} via AP") Logger.info("Fetching #{id} via AP")
with true <- String.starts_with?(id, "http"), with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <- {:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get( @httpoison.get(
id, id,
[Accept: "application/activity+json"], [Accept: "application/activity+json"],

View file

@ -23,7 +23,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do
defp check_media_removal( defp check_media_removal(
%{host: actor_host} = _actor_info, %{host: actor_host} = _actor_info,
%{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object %{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object
) )
when length(child_attachment) > 0 do when length(child_attachment) > 0 do
object = object =

View file

@ -447,7 +447,7 @@ def handle_incoming(
update_data = update_data =
new_user_data new_user_data
|> Map.take([:name, :bio, :avatar]) |> Map.take([:name, :bio, :avatar])
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked})) |> Map.put(:info, %{"banner" => banner, "locked" => locked})
actor actor
|> User.upgrade_changeset(update_data) |> User.upgrade_changeset(update_data)
@ -850,10 +850,6 @@ defp user_upgrade_task(user) do
def upgrade_user_from_ap_id(ap_id, async \\ true) do def upgrade_user_from_ap_id(ap_id, async \\ true) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id), with %User{local: false} = user <- User.get_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
data =
data
|> Map.put(:info, Map.merge(user.info, data[:info]))
already_ap = User.ap_enabled?(user) already_ap = User.ap_enabled?(user)
{:ok, user} = {:ok, user} =

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
# the instance itself is not a Person, but instead an Application # the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
@ -40,7 +40,7 @@ def render("user.json", %{user: %{nickname: nil} = user}) do
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
@ -55,7 +55,7 @@ def render("user.json", %{user: user}) do
"name" => user.name, "name" => user.name,
"summary" => user.bio, "summary" => user.bio,
"url" => user.ap_id, "url" => user.ap_id,
"manuallyApprovesFollowers" => user.info["locked"] || false, "manuallyApprovesFollowers" => user.info.locked,
"publicKey" => %{ "publicKey" => %{
"id" => "#{user.ap_id}#main-key", "id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id, "owner" => user.ap_id,
@ -72,7 +72,7 @@ def render("user.json", %{user: user}) do
"type" => "Image", "type" => "Image",
"url" => User.banner_url(user) "url" => User.banner_url(user)
}, },
"tag" => user.info["source_data"]["tag"] || [] "tag" => user.info.source_data["tag"] || []
} }
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -82,7 +82,7 @@ def render("following.json", %{user: user, page: page}) do
query = from(user in query, select: [:ap_id]) query = from(user in query, select: [:ap_id])
following = Repo.all(query) following = Repo.all(query)
collection(following, "#{user.ap_id}/following", page) collection(following, "#{user.ap_id}/following", page, !user.info.hide_network)
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -95,7 +95,7 @@ def render("following.json", %{user: user}) do
"id" => "#{user.ap_id}/following", "id" => "#{user.ap_id}/following",
"type" => "OrderedCollection", "type" => "OrderedCollection",
"totalItems" => length(following), "totalItems" => length(following),
"first" => collection(following, "#{user.ap_id}/following", 1) "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network)
} }
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -105,7 +105,7 @@ def render("followers.json", %{user: user, page: page}) do
query = from(user in query, select: [:ap_id]) query = from(user in query, select: [:ap_id])
followers = Repo.all(query) followers = Repo.all(query)
collection(followers, "#{user.ap_id}/followers", page) collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network)
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -118,7 +118,7 @@ def render("followers.json", %{user: user}) do
"id" => "#{user.ap_id}/followers", "id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection", "type" => "OrderedCollection",
"totalItems" => length(followers), "totalItems" => length(followers),
"first" => collection(followers, "#{user.ap_id}/followers", 1) "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network)
} }
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -172,7 +172,7 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
end end
end end
def collection(collection, iri, page, total \\ nil) do def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10 offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10) items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end) items = Enum.map(items, fn user -> user.ap_id end)
@ -183,7 +183,7 @@ def collection(collection, iri, page, total \\ nil) do
"type" => "OrderedCollectionPage", "type" => "OrderedCollectionPage",
"partOf" => iri, "partOf" => iri,
"totalItems" => total, "totalItems" => total,
"orderedItems" => items "orderedItems" => if(show_items, do: items, else: [])
} }
if offset < total do if offset < total do

View file

@ -45,21 +45,29 @@ def right_add(conn, %{"permission_group" => permission_group, "nickname" => nick
user = User.get_by_nickname(nickname) user = User.get_by_nickname(nickname)
info = info =
user.info %{}
|> Map.put("is_" <> permission_group, true) |> Map.put("is_" <> permission_group, true)
cng = User.info_changeset(user, %{info: info}) info_cng = User.Info.admin_api_update(user.info, info)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
{:ok, user} = User.update_and_set_cache(cng) {:ok, user} = User.update_and_set_cache(cng)
conn conn
|> json(user.info) |> json(info)
end end
def right_get(conn, %{"nickname" => nickname}) do def right_get(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname) user = User.get_by_nickname(nickname)
conn conn
|> json(user.info) |> json(%{
is_moderator: user.info.is_moderator,
is_admin: user.info.is_admin
})
end end
def right_add(conn, _) do def right_add(conn, _) do
@ -84,14 +92,19 @@ def right_delete(
user = User.get_by_nickname(nickname) user = User.get_by_nickname(nickname)
info = info =
user.info %{}
|> Map.put("is_" <> permission_group, false) |> Map.put("is_" <> permission_group, false)
cng = User.info_changeset(user, %{info: info}) info_cng = User.Info.admin_api_update(user.info, info)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
{:ok, user} = User.update_and_set_cache(cng) {:ok, user} = User.update_and_set_cache(cng)
conn conn
|> json(user.info) |> json(info)
end end
end end

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
def delete(activity_id, user) do def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id), with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.normalize(object_id), %Object{} = object <- Object.normalize(object_id),
true <- user.info["is_moderator"] || user.ap_id == object.data["actor"], true <- user.info.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete} {:ok, delete}
end end
@ -135,12 +135,13 @@ def post(user, %{"status" => status} = data) do
end end
end end
# Updates the emojis for a user based on their profile
def update(user) do def update(user) do
user = user =
with emoji <- emoji_from_profile(user), with emoji <- emoji_from_profile(user),
source_data <- (user.info["source_data"] || %{}) |> Map.put("tag", emoji), source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
new_info <- Map.put(user.info, "source_data", source_data), info_cng <- Pleroma.User.Info.set_source_data(user.info, source_data),
change <- User.info_changeset(user, %{info: new_info}), change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(change) do {:ok, user} <- User.update_and_set_cache(change) do
user user
else else

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
plug(CORSPlug) plug(CORSPlug)
plug(Pleroma.Plugs.HTTPSecurityPlug) plug(Pleroma.Plugs.HTTPSecurityPlug)
plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false) plug(Pleroma.Plugs.UploadedMedia)
plug( plug(
Plug.Static, Plug.Static,

View file

@ -17,7 +17,15 @@ def init(args) do
end end
def start_link() do def start_link() do
GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__) enabled = Pleroma.Config.get([:retry_queue, :enabled], false)
if enabled do
Logger.info("Starting retry queue")
GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
else
Logger.info("Retry queue disabled")
:ignore
end
end end
def enqueue(data, transport, retries \\ 0) do def enqueue(data, transport, retries \\ 0) do

View file

@ -65,7 +65,7 @@ def build_signing_string(headers, used_headers) do
end end
def sign(user, headers) do def sign(user, headers) do
with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user), with {:ok, %{info: %{keys: keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do {:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
sigstring = build_signing_string(headers, Map.keys(headers)) sigstring = build_signing_string(headers, Map.keys(headers))

View file

@ -32,75 +32,55 @@ def create_app(conn, params) do
end end
end end
defp add_if_present(
map,
params,
params_field,
map_field,
value_function \\ fn x -> {:ok, x} end
) do
if Map.has_key?(params, params_field) do
case value_function.(params[params_field]) do
{:ok, new_value} -> Map.put(map, map_field, new_value)
:error -> map
end
else
map
end
end
def update_credentials(%{assigns: %{user: user}} = conn, params) do def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user original_user = user
avatar_upload_limit = user_params =
Application.get_env(:pleroma, :instance) %{}
|> Keyword.fetch(:avatar_upload_limit) |> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
banner_upload_limit = |> add_if_present(params, "avatar", :avatar, fn value ->
Application.get_env(:pleroma, :instance) with %Plug.Upload{} <- value,
|> Keyword.fetch(:banner_upload_limit) {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
{:ok, object.data}
params =
if bio = params["note"] do
Map.put(params, "bio", bio)
else
params
end
params =
if name = params["display_name"] do
Map.put(params, "name", name)
else
params
end
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
else else
_e -> user _ -> :error
end end
else end)
user
end
user = info_params =
if banner = params["header"] do %{}
with %Plug.Upload{} <- banner, |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
{:ok, object} <- ActivityPub.upload(banner, banner_upload_limit), |> add_if_present(params, "header", :banner, fn value ->
new_info <- Map.put(user.info, "banner", object.data), with %Plug.Upload{} <- value,
change <- User.info_changeset(user, %{info: new_info}), {:ok, object} <- ActivityPub.upload(value, type: :banner) do
{:ok, user} <- User.update_and_set_cache(change) do {:ok, object.data}
user
else else
_e -> user _ -> :error
end end
else end)
user
end
user = info_cng = User.Info.mastodon_profile_update(user.info, info_params)
if locked = params["locked"] do
with locked <- locked == "true",
new_info <- Map.put(user.info, "locked", locked),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do if original_user != user do
CommonAPI.update(user) CommonAPI.update(user)
@ -453,40 +433,31 @@ def relationships(%{assigns: %{user: user}} = conn, _) do
|> json([]) |> json([])
end end
def update_media(%{assigns: %{user: _}} = conn, data) do def update_media(%{assigns: %{user: user}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]), with %Object{} = object <- Repo.get(Object, data["id"]),
true <- Object.authorize_mutation(object, user),
true <- is_binary(data["description"]), true <- is_binary(data["description"]),
description <- data["description"] do description <- data["description"] do
new_data = %{object.data | "name" => description} new_data = %{object.data | "name" => description}
change = Object.change(object, %{data: new_data}) {:ok, _} =
{:ok, _} = Repo.update(change) object
|> Object.change(%{data: new_data})
|> Repo.update()
data = attachment_data = Map.put(new_data, "id", object.id)
new_data render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: data})
end end
end end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <- ActivityPub.upload(file) do with {:ok, object} <-
objdata = ActivityPub.upload(file,
if Map.has_key?(data, "description") do actor: User.ap_id(user),
Map.put(object.data, "name", data["description"]) description: Map.get(data, "description")
else ) do
object.data attachment_data = Map.put(object.data, "id", object.id)
end render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
change = Object.change(object, %{data: objdata})
{:ok, object} = Repo.update(change)
objdata =
objdata
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: objdata})
end end
end end
@ -529,17 +500,30 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end end
# TODO: Pagination def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
def followers(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do {:ok, followers} <- User.get_followers(user) do
followers =
cond do
for_user && user.id == for_user.id -> followers
user.info.hide_network -> []
true -> followers
end
render(conn, AccountView, "accounts.json", %{users: followers, as: :user}) render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end end
end end
def following(conn, %{"id" => id}) do def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do {:ok, followers} <- User.get_friends(user) do
followers =
cond do
for_user && user.id == for_user.id -> followers
user.info.hide_network -> []
true -> followers
end
render(conn, AccountView, "accounts.json", %{users: followers, as: :user}) render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end end
end end
@ -659,7 +643,7 @@ def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
# TODO: Use proper query # TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_users <- user.info["blocks"] || [], with blocked_users <- user.info.blocks || [],
accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res) json(conn, res)
@ -667,7 +651,7 @@ def blocks(%{assigns: %{user: user}} = conn, _) do
end end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, info["domain_blocks"] || []) json(conn, info.domain_blocks || [])
end end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
@ -915,11 +899,11 @@ def index(%{assigns: %{user: user}} = conn, _params) do
max_toot_chars: limit max_toot_chars: limit
}, },
rights: %{ rights: %{
delete_others_notice: !!user.info["is_moderator"] delete_others_notice: !!user.info.is_moderator
}, },
compose: %{ compose: %{
me: "#{user.id}", me: "#{user.id}",
default_privacy: user.info["default_scope"] || "public", default_privacy: user.info.default_scope,
default_sensitive: false default_sensitive: false
}, },
media_attachments: %{ media_attachments: %{
@ -939,7 +923,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
] ]
}, },
settings: settings:
Map.get(user.info, "settings") || Map.get(user.info, :settings) ||
%{ %{
onboarded: true, onboarded: true,
home: %{ home: %{
@ -1224,7 +1208,7 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
user = user.nickname user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user) url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
with {:ok, %{status_code: 200, body: body}} <- with {:ok, %{status: 200, body: body}} <-
@httpoison.get(url, [], timeout: timeout, recv_timeout: timeout), @httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
{:ok, data} <- Jason.decode(body) do {:ok, data} <- Jason.decode(body) do
data2 = data2 =

View file

@ -14,10 +14,10 @@ def render("account.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url() image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user) user_info = User.user_info(user)
bot = (user.info["source_data"]["type"] || "Person") in ["Application", "Service"] bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
emojis = emojis =
(user.info["source_data"]["tag"] || []) (user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
%{ %{
@ -29,7 +29,7 @@ def render("account.json", %{user: user} = opts) do
end) end)
fields = fields =
(user.info["source_data"]["attachment"] || []) (user.info.source_data["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)

View file

@ -1,135 +1,34 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
require Logger alias Pleroma.{Web.MediaProxy, ReverseProxy}
@httpoison Application.get_env(:pleroma, :httpoison) @default_proxy_opts [max_body_length: 25 * 1_048_576]
@max_body_length 25 * 1_048_576 def remote(conn, params = %{"sig" => sig64, "url" => url64}) do
with config <- Pleroma.Config.get([:media_proxy], []),
@cache_control %{ true <- Keyword.get(config, :enabled, false),
default: "public, max-age=1209600", {:ok, url} <- MediaProxy.decode_url(sig64, url64),
error: "public, must-revalidate, max-age=160"
}
# Content-types that will not be returned as content-disposition attachments
# Override with :media_proxy, :safe_content_types in the configuration
@safe_content_types [
"image/gif",
"image/jpeg",
"image/jpg",
"image/png",
"image/svg+xml",
"audio/mpeg",
"audio/mp3",
"video/webm",
"video/mp4"
]
def remote(conn, params = %{"sig" => sig, "url" => url}) do
config = Application.get_env(:pleroma, :media_proxy, [])
with true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
filename <- Path.basename(URI.parse(url).path), filename <- Path.basename(URI.parse(url).path),
true <- :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
if(Map.get(params, "filename"), ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
do: filename == Path.basename(conn.request_path),
else: true
),
{:ok, content_type, body} <- proxy_request(url),
safe_content_type <-
Enum.member?(
Keyword.get(config, :safe_content_types, @safe_content_types),
content_type
) do
conn
|> put_resp_content_type(content_type)
|> set_cache_header(:default)
|> put_resp_header(
"content-security-policy",
"default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:"
)
|> put_resp_header("x-xss-protection", "1; mode=block")
|> put_resp_header("x-content-type-options", "nosniff")
|> put_attachement_header(safe_content_type, filename)
|> send_resp(200, body)
else else
false -> false ->
send_error(conn, 404) send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
{:error, :invalid_signature} -> {:error, :invalid_signature} ->
send_error(conn, 403) send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
{:error, {:http, _, url}} -> {:wrong_filename, filename} ->
redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true)) redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
end end
end end
defp proxy_request(link) do def filename_matches(has_filename, path, url) do
headers = [ filename = MediaProxy.filename(url)
{"user-agent",
"Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{
Application.get_env(:pleroma, :instance)[:email]
}>"}
]
options = cond do
@httpoison.process_request_options([:insecure, {:follow_redirect, true}]) ++ has_filename && filename && Path.basename(path) != filename -> {:wrong_filename, filename}
[{:pool, :default}] true -> :ok
with {:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
headers = Enum.into(headers, Map.new()),
{:ok, body} <- proxy_request_body(client),
content_type <- proxy_request_content_type(headers, body) do
{:ok, content_type, body}
else
{:ok, status, _, _} ->
Logger.warn("MediaProxy: request failed, status #{status}, link: #{link}")
{:error, {:http, :bad_status, link}}
{:error, error} ->
Logger.warn("MediaProxy: request failed, error #{inspect(error)}, link: #{link}")
{:error, {:http, error, link}}
end end
end end
defp set_cache_header(conn, key) do
Plug.Conn.put_resp_header(conn, "cache-control", @cache_control[key])
end
defp redirect_or_error(conn, url, true), do: redirect(conn, external: url)
defp redirect_or_error(conn, url, _), do: send_error(conn, 502, "Media proxy error: " <> url)
defp send_error(conn, code, body \\ "") do
conn
|> set_cache_header(:error)
|> send_resp(code, body)
end
defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
case :hackney.stream_body(client) do
{:ok, data} -> proxy_request_body(client, <<body::binary, data::binary>>)
:done -> {:ok, body}
{:error, reason} -> {:error, reason}
end
end
defp proxy_request_body(client, _) do
:hackney.close(client)
{:error, :body_too_large}
end
# TODO: the body is passed here as well because some hosts do not provide a content-type.
# At some point we may want to use magic numbers to discover the content-type and reply a proper one.
defp proxy_request_content_type(headers, _body) do
headers["Content-Type"] || headers["content-type"] || "application/octet-stream"
end
defp put_attachement_header(conn, true, _), do: conn
defp put_attachement_header(conn, false, filename) do
put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'")
end
end end

View file

@ -17,10 +17,8 @@ def url(url) do
base64 = Base.url_encode64(url, @base64_opts) base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64) sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts) sig64 = sig |> Base.url_encode64(@base64_opts)
filename = if path = URI.parse(url).path, do: "/" <> Path.basename(path), else: ""
Keyword.get(config, :base_url, Pleroma.Web.base_url()) <> build_url(sig64, base64, filename(url))
"/proxy/#{sig64}/#{base64}#{filename}"
end end
end end
@ -35,4 +33,20 @@ def decode_url(sig, url) do
{:error, :invalid_signature} {:error, :invalid_signature}
end end
end end
def filename(url_or_path) do
if path = URI.parse(url_or_path).path, do: Path.basename(path)
end
def build_url(sig_base64, url_base64, filename \\ nil) do
[
Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()),
"proxy",
sig_base64,
url_base64,
filename
]
|> Enum.filter(fn value -> value end)
|> Path.join()
end
end end

View file

@ -226,25 +226,21 @@ def maybe_update_ostatus(doc, user) do
old_data = %{ old_data = %{
avatar: user.avatar, avatar: user.avatar,
bio: user.bio, bio: user.bio,
name: user.name, name: user.name
info: user.info
} }
with false <- user.local, with false <- user.local,
avatar <- make_avatar_object(doc), avatar <- make_avatar_object(doc),
bio <- string_from_xpath("//author[1]/summary", doc), bio <- string_from_xpath("//author[1]/summary", doc),
name <- string_from_xpath("//author[1]/poco:displayName", doc), name <- string_from_xpath("//author[1]/poco:displayName", doc),
info <-
Map.put(user.info, "banner", make_avatar_object(doc, "header") || user.info["banner"]),
new_data <- %{ new_data <- %{
avatar: avatar || old_data.avatar, avatar: avatar || old_data.avatar,
name: name || old_data.name, name: name || old_data.name,
bio: bio || old_data.bio, bio: bio || old_data.bio
info: info || old_data.info
}, },
false <- new_data == old_data do false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data) change = Ecto.Changeset.change(user, new_data)
Repo.update(change) User.update_and_set_cache(change)
else else
_ -> _ ->
{:ok, user} {:ok, user}
@ -350,13 +346,15 @@ def get_atom_url(body) do
def fetch_activity_from_atom_url(url) do def fetch_activity_from_atom_url(url) do
with true <- String.starts_with?(url, "http"), with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <- {:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get( @httpoison.get(
url, url,
[Accept: "application/atom+xml"], [Accept: "application/atom+xml"],
follow_redirect: true, follow_redirect: true,
timeout: 10000, adapter: [
recv_timeout: 20000 timeout: 10000,
recv_timeout: 20000
]
) do ) do
Logger.debug("Got document from #{url}, handling...") Logger.debug("Got document from #{url}, handling...")
handle_incoming(body) handle_incoming(body)

View file

@ -302,12 +302,6 @@ defmodule Pleroma.Web.Router do
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner) post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background) post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
post(
"/account/most_recent_notification",
TwitterAPI.Controller,
:update_most_recent_notification
)
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline) get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline) get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline) get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
@ -335,6 +329,7 @@ defmodule Pleroma.Web.Router do
post("/statusnet/media/upload", TwitterAPI.Controller, :upload) post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json) post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/media/metadata/create", TwitterAPI.Controller, :update_media)
post("/favorites/create/:id", TwitterAPI.Controller, :favorite) post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite) post("/favorites/create", TwitterAPI.Controller, :favorite)

View file

@ -157,15 +157,17 @@ def remote_users(%{data: %{"to" => to} = data}) do
|> Enum.filter(fn user -> user && !user.local end) |> Enum.filter(fn user -> user && !user.local end)
end end
defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do defp send_to_user(%{info: %{salmon: salmon}}, feed, poster) do
with {:ok, %{status_code: code}} <- with {:ok, %{status: code}} <-
poster.( poster.(
salmon, salmon,
feed, feed,
[{"Content-Type", "application/magic-envelope+xml"}], [{"Content-Type", "application/magic-envelope+xml"}],
timeout: 10000, adapter: [
recv_timeout: 20000, timeout: 10000,
hackney: [pool: :default] recv_timeout: 20000,
pool: :default
]
) do ) do
Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end) Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end)
else else
@ -185,7 +187,7 @@ defp send_to_user(_, _, _), do: nil
] ]
def publish(user, activity, poster \\ &@httpoison.post/4) def publish(user, activity, poster \\ &@httpoison.post/4)
def publish(%{info: %{"keys" => keys}} = user, %{data: %{"type" => type}} = activity, poster) def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster)
when type in @supported_activities do when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true) feed = ActivityRepresenter.to_simple_form(activity, user, true)

View file

@ -188,7 +188,7 @@ def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = ite
# Get the current user so we have up-to-date blocks etc. # Get the current user so we have up-to-date blocks etc.
if socket.assigns[:user] do if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id) user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || [] blocks = user.info.blocks || []
parent = Object.normalize(item.data["object"]) parent = Object.normalize(item.data["object"])
@ -206,7 +206,7 @@ def push_to_socket(topics, topic, item) do
# Get the current user so we have up-to-date blocks etc. # Get the current user so we have up-to-date blocks etc.
if socket.assigns[:user] do if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id) user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || [] blocks = user.info.blocks || []
unless item.actor in blocks do unless item.actor in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)}) send(socket.transport_pid, {:text, represent_update(item, user)})

View file

@ -93,11 +93,11 @@ def unfav(%User{} = user, ap_id_or_id) do
end end
end end
def upload(%Plug.Upload{} = file, format \\ "xml") do def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
{:ok, object} = ActivityPub.upload(file) {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
url = List.first(object.data["url"]) url = List.first(object.data["url"])
href = url["href"] |> MediaProxy.url() href = url["href"]
type = url["mediaType"] type = url["mediaType"]
case format do case format do
@ -132,7 +132,7 @@ def register_user(params) do
params = %{ params = %{
nickname: params["nickname"], nickname: params["nickname"],
name: params["fullname"], name: params["fullname"],
bio: params["bio"], bio: User.parse_bio(params["bio"]),
email: params["email"], email: params["email"],
password: params["password"], password: params["password"],
password_confirmation: params["confirm"] password_confirmation: params["confirm"]
@ -148,7 +148,7 @@ def register_user(params) do
cond do cond do
registrations_open || (!is_nil(token) && !token.used) -> registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{}, params) changeset = User.register_changeset(%User{info: %{}}, params)
with {:ok, user} <- Repo.insert(changeset) do with {:ok, user} <- Repo.insert(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token) !registrations_open && UserInviteToken.mark_as_used(token.token)
@ -279,14 +279,6 @@ def conversation_id_to_context(id) do
def get_external_profile(for_user, uri) do def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do with %User{} = user <- User.get_or_fetch(uri) do
spawn(fn ->
with url <- user.info["topic"],
{:ok, %{body: body}} <-
@httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
OStatus.handle_incoming(body)
end
end)
{:ok, UserView.render("show.json", %{user: user, for: for_user})} {:ok, UserView.render("show.json", %{user: user, for: for_user})}
else else
_e -> _e ->

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView} alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.{Repo, Activity, Object, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Ecto.Changeset alias Ecto.Changeset
@ -226,16 +226,51 @@ def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
end end
def upload(conn, %{"media" => media}) do @doc """
response = TwitterAPI.upload(media) Updates metadata of uploaded media object.
Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
"""
def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
object = Repo.get(Object, id)
description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
{conn, status, response_body} =
cond do
!object ->
{halt(conn), :not_found, ""}
!Object.authorize_mutation(object, user) ->
{halt(conn), :forbidden, "You can only update your own uploads."}
!is_binary(description) ->
{conn, :not_modified, ""}
true ->
new_data = Map.put(object.data, "name", description)
{:ok, _} =
object
|> Object.change(%{data: new_data})
|> Repo.update()
{conn, :no_content, ""}
end
conn
|> put_status(status)
|> json(response_body)
end
def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
response = TwitterAPI.upload(media, user)
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/atom+xml")
|> send_resp(200, response) |> send_resp(200, response)
end end
def upload_json(conn, %{"media" => media}) do def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
response = TwitterAPI.upload(media, "json") response = TwitterAPI.upload(media, user, "json")
conn conn
|> json_reply(200, response) |> json_reply(200, response)
@ -290,11 +325,7 @@ def register(conn, params) do
end end
def update_avatar(%{assigns: %{user: user}} = conn, params) do def update_avatar(%{assigns: %{user: user}} = conn, params) do
upload_limit = {:ok, object} = ActivityPub.upload(params, type: :avatar)
Application.get_env(:pleroma, :instance)
|> Keyword.fetch(:avatar_upload_limit)
{:ok, object} = ActivityPub.upload(params, upload_limit)
change = Changeset.change(user, %{avatar: object.data}) change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change) {:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user) CommonAPI.update(user)
@ -303,14 +334,11 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do
end end
def update_banner(%{assigns: %{user: user}} = conn, params) do def update_banner(%{assigns: %{user: user}} = conn, params) do
upload_limit = with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
Application.get_env(:pleroma, :instance) new_info <- %{"banner" => object.data},
|> Keyword.fetch(:banner_upload_limit) info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, upload_limit), {:ok, user} <- User.update_and_set_cache(changeset) do
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
CommonAPI.update(user) CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!() response = %{url: href} |> Jason.encode!()
@ -321,14 +349,11 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
end end
def update_background(%{assigns: %{user: user}} = conn, params) do def update_background(%{assigns: %{user: user}} = conn, params) do
upload_limit = with {:ok, object} <- ActivityPub.upload(params, type: :background),
Application.get_env(:pleroma, :instance) new_info <- %{"background" => object.data},
|> Keyword.fetch(:background_upload_limit) info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
with {:ok, object} <- ActivityPub.upload(params, upload_limit), {:ok, _user} <- User.update_and_set_cache(changeset) do
new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do
%{"url" => [%{"href" => href} | _]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!() response = %{url: href} |> Jason.encode!()
@ -350,32 +375,32 @@ def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" =>
end end
end end
def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" => id}) do def followers(%{assigns: %{user: for_user}} = conn, params) do
with id when is_number(id) <- String.to_integer(id), with {:ok, user} <- TwitterAPI.get_user(for_user, params),
info <- user.info,
mrn <- max(id, user.info["most_recent_notification"] || 0),
updated_info <- Map.put(info, "most_recent_notification", mrn),
changeset <- User.info_changeset(user, %{info: updated_info}),
{:ok, _user} <- User.update_and_set_cache(changeset) do
conn
|> json_reply(200, Jason.encode!(mrn))
else
_e -> bad_request_reply(conn, "Can't update.")
end
end
def followers(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, followers} <- User.get_followers(user) do {:ok, followers} <- User.get_followers(user) do
followers =
cond do
for_user && user.id == for_user.id -> followers
user.info.hide_network -> []
true -> followers
end
render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]}) render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
else else
_e -> bad_request_reply(conn, "Can't get followers") _e -> bad_request_reply(conn, "Can't get followers")
end end
end end
def friends(conn, params) do def friends(%{assigns: %{user: for_user}} = conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params), with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friends} <- User.get_friends(user) do {:ok, friends} <- User.get_friends(user) do
friends =
cond do
for_user && user.id == for_user.id -> friends
user.info.hide_network -> []
true -> friends
end
render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]}) render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
else else
_e -> bad_request_reply(conn, "Can't get friends") _e -> bad_request_reply(conn, "Can't get friends")
@ -451,67 +476,41 @@ def raw_empty_array(conn, _params) do
json(conn, []) json(conn, [])
end end
defp build_info_cng(user, params) do
info_params =
["no_rich_text", "locked", "hide_network"]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
else
res
end
end)
info_params =
if value = params["default_scope"] do
Map.put(info_params, "default_scope", value)
else
info_params
end
User.Info.profile_update(user.info, info_params)
end
defp parse_profile_bio(user, params) do
if bio = params["description"] do
Map.put(params, "bio", User.parse_bio(bio, user))
else
params
end
end
def update_profile(%{assigns: %{user: user}} = conn, params) do def update_profile(%{assigns: %{user: user}} = conn, params) do
params = params = parse_profile_bio(user, params)
if bio = params["description"] do info_cng = build_info_cng(user, params)
mentions = Formatter.parse_mentions(bio)
tags = Formatter.parse_tags(bio)
emoji =
(user.info["source_data"]["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain")
Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
else
params
end
user =
if locked = params["locked"] do
with locked <- locked == "true",
new_info <- Map.put(user.info, "locked", locked),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
user =
if no_rich_text = params["no_rich_text"] do
with no_rich_text <- no_rich_text == "true",
new_info <- Map.put(user.info, "no_rich_text", no_rich_text),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
user =
if default_scope = params["default_scope"] do
with new_info <- Map.put(user.info, "default_scope", default_scope),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user) CommonAPI.update(user)
render(conn, UserView, "user.json", %{user: user, for: user}) render(conn, UserView, "user.json", %{user: user, for: user})

View file

@ -31,7 +31,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
user_info = User.get_cached_user_info(user) user_info = User.get_cached_user_info(user)
emoji = emoji =
(user.info["source_data"]["tag"] || []) (user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url} {String.trim(name, ":"), url}
@ -40,7 +40,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …] # For example: [{"name": "Pronoun", "value": "she/her"}, …]
fields = fields =
(user.info["source_data"]["attachment"] || []) (user.info.source_data["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
@ -66,17 +66,17 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"profile_image_url_profile_size" => image, "profile_image_url_profile_size" => image,
"profile_image_url_original" => image, "profile_image_url_original" => image,
"rights" => %{ "rights" => %{
"delete_others_notice" => !!user.info["is_moderator"] "delete_others_notice" => !!user.info.is_moderator
}, },
"screen_name" => user.nickname, "screen_name" => user.nickname,
"statuses_count" => user_info[:note_count], "statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id, "statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(), "cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(), "background_image" => image_url(user.info.background) |> MediaProxy.url(),
"is_local" => user.local, "is_local" => user.local,
"locked" => !!user.info["locked"], "locked" => user.info.locked,
"default_scope" => user.info["default_scope"] || "public", "default_scope" => user.info.default_scope,
"no_rich_text" => user.info["no_rich_text"] || false, "no_rich_text" => user.info.no_rich_text,
"fields" => fields "fields" => fields
} }

View file

@ -45,7 +45,7 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
def represent_user(user, "JSON") do def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public) magic_key = Salmon.encode_key(public)
%{ %{
@ -83,7 +83,7 @@ def represent_user(user, "JSON") do
def represent_user(user, "XML") do def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public) magic_key = Salmon.encode_key(public)
{ {
@ -113,16 +113,22 @@ def represent_user(user, "XML") do
# This seems a better fit in Salmon # This seems a better fit in Salmon
def ensure_keys_present(user) do def ensure_keys_present(user) do
info = user.info || %{} info = user.info
if info["keys"] do if info.keys do
{:ok, user} {:ok, user}
else else
{:ok, pem} = Salmon.generate_rsa_pem() {:ok, pem} = Salmon.generate_rsa_pem()
info = Map.put(info, "keys", pem)
Ecto.Changeset.change(user, info: info) info_cng =
|> User.update_and_set_cache() info
|> Pleroma.User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
User.update_and_set_cache(cng)
end end
end end
@ -214,7 +220,7 @@ def get_template_from_xml(body) do
end end
def find_lrdd_template(domain) do def find_lrdd_template(domain) do
with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- with {:ok, %{status: status, body: body}} when status in 200..299 <-
@httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
get_template_from_xml(body) get_template_from_xml(body)
else else
@ -253,7 +259,7 @@ def finger(account) do
[Accept: "application/xrd+xml,application/jrd+json"], [Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true follow_redirect: true
), ),
{:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do {:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body) doc = XML.parse_document(body)
if doc != :error do if doc != :error do

View file

@ -146,7 +146,7 @@ defp valid_topic(%{"hub.topic" => topic}, user) do
end end
def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
topic = subscribed.info["topic"] topic = subscribed.info.topic
# FIXME: Race condition, use transactions # FIXME: Race condition, use transactions
{:ok, subscription} = {:ok, subscription} =
with subscription when not is_nil(subscription) <- with subscription when not is_nil(subscription) <-
@ -158,7 +158,7 @@ def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
_e -> _e ->
subscription = %WebsubClientSubscription{ subscription = %WebsubClientSubscription{
topic: topic, topic: topic,
hub: subscribed.info["hub"], hub: subscribed.info.hub,
subscribers: [subscriber.ap_id], subscribers: [subscriber.ap_id],
state: "requested", state: "requested",
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(), secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
@ -173,7 +173,7 @@ def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
def gather_feed_data(topic, getter \\ &@httpoison.get/1) do def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
with {:ok, response} <- getter.(topic), with {:ok, response} <- getter.(topic),
status_code when status_code in 200..299 <- response.status_code, status when status in 200..299 <- response.status,
body <- response.body, body <- response.body,
doc <- XML.parse_document(body), doc <- XML.parse_document(body),
uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc), uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
@ -221,7 +221,7 @@ def request_subscription(websub, poster \\ &@httpoison.post/3, timeout \\ 10_000
task = Task.async(websub_checker) task = Task.async(websub_checker)
with {:ok, %{status_code: 202}} <- with {:ok, %{status: 202}} <-
poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"), poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"),
{:ok, websub} <- Task.yield(task, timeout) do {:ok, websub} <- Task.yield(task, timeout) do
{:ok, websub} {:ok, websub}
@ -257,7 +257,7 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) d
signature = sign(secret || "", xml) signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end) Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
with {:ok, %{status_code: code}} <- with {:ok, %{status: code}} <-
@httpoison.post( @httpoison.post(
callback, callback,
xml, xml,
@ -265,9 +265,11 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) d
{"Content-Type", "application/atom+xml"}, {"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"} {"X-Hub-Signature", "sha1=#{signature}"}
], ],
timeout: 10000, adapter: [
recv_timeout: 20000, timeout: 10000,
hackney: [pool: :default] recv_timeout: 20000,
pool: :default
]
) do ) do
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end) Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
{:ok, code} {:ok, code}

View file

@ -56,6 +56,7 @@ defp deps do
{:calendar, "~> 0.17.4"}, {:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0.2"}, {:cachex, "~> 3.0.2"},
{:httpoison, "~> 1.2.0"}, {:httpoison, "~> 1.2.0"},
{:tesla, "~> 1.2"},
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"}, {:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.0"}, {:ex_aws, "~> 2.0"},

View file

@ -49,6 +49,7 @@
"postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddUUIDExtension do
use Ecto.Migration
def change do
execute("create extension if not exists \"uuid-ossp\"")
end
end

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddUUIDsToUserInfo do
use Ecto.Migration
def change do
execute("update users set info = jsonb_set(info, '{\"id\"}', to_jsonb(uuid_generate_v4()))")
end
end

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.0808aeafc6252b3050ea95b17dcaff1a.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.34667c2817916147413f.js></script><script type=text/javascript src=/static/js/vendor.32c621c7157f34c20923.js></script><script type=text/javascript src=/static/js/app.065638d22ade92dea420.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.0808aeafc6252b3050ea95b17dcaff1a.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.18df0da570d88ba76ec5.js></script><script type=text/javascript src=/static/js/vendor.0e895ca116d5ba12f2b6.js></script><script type=text/javascript src=/static/js/app.3f7c9aaedc6b87fa9653.js></script></body></html>

View file

@ -11,6 +11,8 @@
"scopeOptionsEnabled": false, "scopeOptionsEnabled": false,
"formattingOptionsEnabled": false, "formattingOptionsEnabled": false,
"collapseMessageWithSubject": false, "collapseMessageWithSubject": false,
"scopeCopy": false,
"subjectLineBehavior": "email",
"hidePostStats": false, "hidePostStats": false,
"hideUserStats": false, "hideUserStats": false,
"loginMethod": "password" "loginMethod": "password"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -53,4 +53,19 @@ test "put/2 with a list of keys" do
assert Pleroma.Config.get([:instance, :config_test]) == true assert Pleroma.Config.get([:instance, :config_test]) == true
assert Pleroma.Config.get([:instance, :config_nested_test, :x]) == true assert Pleroma.Config.get([:instance, :config_nested_test, :x]) == true
end end
test "delete/1 with a key" do
Pleroma.Config.put([:delete_me], :delete_me)
Pleroma.Config.delete([:delete_me])
assert Pleroma.Config.get([:delete_me]) == nil
end
test "delete/2 with a list of keys" do
Pleroma.Config.put([:delete_me], hello: "world", world: "Hello")
Pleroma.Config.delete([:delete_me, :world])
assert Pleroma.Config.get([:delete_me]) == [hello: "world"]
Pleroma.Config.put([:delete_me, :delete_me], hello: "world", world: "Hello")
Pleroma.Config.delete([:delete_me, :delete_me, :world])
assert Pleroma.Config.get([:delete_me, :delete_me]) == [hello: "world"]
end
end end

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">framatube.org</hm:Host><Link rel="lrdd" template="http://framatube.org/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD>

View file

@ -0,0 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?><XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'
xmlns:hm='http://host-meta.net/xrd/1.0'>
<hm:Host>gerzilla.de</hm:Host>
<Link rel='lrdd' type="application/xrd+xml" template='https://gerzilla.de/xrd/?uri={uri}' />
<Link rel="http://oexchange.org/spec/0.8/rel/resident-target" type="application/xrd+xml"
href="https://gerzilla.de/oexchange/xrd" />
</XRD>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">gnusocial.de</hm:Host><Link rel="lrdd" template="http://gnusocial.de/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD>

View file

@ -5,12 +5,17 @@ defmodule Pleroma.FormatterTest do
import Pleroma.Factory import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe ".add_hashtag_links" do describe ".add_hashtag_links" do
test "turns hashtags into links" do test "turns hashtags into links" do
text = "I love #cofe and #2hu" text = "I love #cofe and #2hu"
expected_text = expected_text =
"I love <a href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>" "I love <a data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>"
tags = Formatter.parse_tags(text) tags = Formatter.parse_tags(text)
@ -110,7 +115,7 @@ test "gives a replacement for user links" do
archaeme = archaeme =
insert(:user, %{ insert(:user, %{
nickname: "archaeme", nickname: "archaeme",
info: %{"source_data" => %{"url" => "https://archeme/@archaeme"}} info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archaeme"}}
}) })
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
@ -123,11 +128,11 @@ test "gives a replacement for user links" do
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text = expected_text =
"<span><a class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a class='mention' href='#{ "<span><a data-user='#{gsimg.id}' class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a data-user='#{
"https://archeme/@archaeme" archaeme.id
}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a class='mention' href='#{ }' class='mention' href='#{"https://archeme/@archaeme"}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a data-user='#{
archaeme_remote.ap_id archaeme_remote.id
}'>@<span>archaeme</span></a></span>" }' class='mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>"
assert expected_text == Formatter.finalize({subs, text}) assert expected_text == Formatter.finalize({subs, text})
end end
@ -145,7 +150,7 @@ test "gives a replacement for user links when the user is using Osada" do
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text = expected_text =
"<span><a class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test" "<span><a data-user='#{mike.id}' class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test"
assert expected_text == Formatter.finalize({subs, text}) assert expected_text == Formatter.finalize({subs, text})
end end
@ -161,7 +166,9 @@ test "gives a replacement for single-character local nicknames" do
assert length(subs) == 1 assert length(subs) == 1
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text = "<span><a class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi" expected_text =
"<span><a data-user='#{o.id}' class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi"
assert expected_text == Formatter.finalize({subs, text}) assert expected_text == Formatter.finalize({subs, text})
end end

55
test/http_test.exs Normal file
View file

@ -0,0 +1,55 @@
defmodule Pleroma.HTTPTest do
use Pleroma.DataCase
import Tesla.Mock
setup do
mock(fn
%{
method: :get,
url: "http://example.com/hello",
headers: [{"content-type", "application/json"}]
} ->
json(%{"my" => "data"})
%{method: :get, url: "http://example.com/hello"} ->
%Tesla.Env{status: 200, body: "hello"}
%{method: :post, url: "http://example.com/world"} ->
%Tesla.Env{status: 200, body: "world"}
end)
:ok
end
describe "get/1" do
test "returns successfully result" do
assert Pleroma.HTTP.get("http://example.com/hello") == {
:ok,
%Tesla.Env{status: 200, body: "hello"}
}
end
end
describe "get/2 (with headers)" do
test "returns successfully result for json content-type" do
assert Pleroma.HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==
{
:ok,
%Tesla.Env{
status: 200,
body: "{\"my\":\"data\"}",
headers: [{"content-type", "application/json"}]
}
}
end
end
describe "post/2" do
test "returns successfully result" do
assert Pleroma.HTTP.post("http://example.com/world", "") == {
:ok,
%Tesla.Env{status: 200, body: "world"}
}
end
end
end

View file

@ -82,6 +82,23 @@ test "validates signature" do
[_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/")
assert decode_url(sig, base64) == {:error, :invalid_signature} assert decode_url(sig, base64) == {:error, :invalid_signature}
end end
test "uses the configured base_url" do
base_url = Pleroma.Config.get([:media_proxy, :base_url])
if base_url do
on_exit(fn ->
Pleroma.Config.put([:media_proxy, :base_url], base_url)
end)
end
Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social")
url = "https://pleroma.soykaf.com/static/logo.png"
encoded = url(url)
assert String.starts_with?(encoded, Pleroma.Config.get([:media_proxy, :base_url]))
end
end end
describe "when disabled" do describe "when disabled" do

View file

@ -0,0 +1,56 @@
defmodule Pleroma.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Plugs.OAuthPlug
import Pleroma.Factory
@session_opts [
store: :cookie,
key: "_test",
signing_salt: "cooldude"
]
setup %{conn: conn} do
user = insert(:user)
{:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create_token(insert(:oauth_app), user)
%{user: user, token: token, conn: conn}
end
test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do
conn =
conn
|> put_req_header("authorization", "BEARER #{opts[:token]}")
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do
conn =
conn
|> put_req_header("authorization", "bearer #{opts[:token]}")
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
test "with invalid token, it not assigns the user", %{conn: conn} do
conn =
conn
|> put_req_header("authorization", "bearer TTTTT")
|> OAuthPlug.call(%{})
refute conn.assigns[:user]
end
test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do
conn =
conn
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
|> put_session(:oauth_token, opts[:token])
|> OAuthPlug.call(%{})
assert conn.assigns[:user] == opts[:user]
end
end

View file

@ -13,7 +13,7 @@ test "doesn't do anything if the user isn't set", %{conn: conn} do
end end
test "with a user that is deactivated, it removes that user", %{conn: conn} do test "with a user that is deactivated, it removes that user", %{conn: conn} do
user = insert(:user, info: %{"deactivated" => true}) user = insert(:user, info: %{deactivated: true})
conn = conn =
conn conn

View file

@ -5,7 +5,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do
import Pleroma.Factory import Pleroma.Factory
test "accepts a user that is admin", %{conn: conn} do test "accepts a user that is admin", %{conn: conn} do
user = insert(:user, info: %{"is_admin" => true}) user = insert(:user, info: %{is_admin: true})
conn = conn =
build_conn() build_conn()

View file

@ -36,6 +36,23 @@ defmodule Pleroma.DataCase do
:ok :ok
end end
def ensure_local_uploader(_context) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
filters = Pleroma.Config.get([Pleroma.Upload, :filters])
unless uploader == Pleroma.Uploaders.Local || filters != [] do
Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
Pleroma.Config.put([Pleroma.Upload, :filters], [])
on_exit(fn ->
Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
Pleroma.Config.put([Pleroma.Upload, :filters], filters)
end)
end
:ok
end
@doc """ @doc """
A helper that transform changeset errors to a map of messages. A helper that transform changeset errors to a map of messages.

View file

@ -7,7 +7,8 @@ def user_factory do
email: sequence(:email, &"user#{&1}@example.com"), email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"), nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}") bio: sequence(:bio, &"Tester Number #{&1}"),
info: %{}
} }
%{ %{

View file

@ -0,0 +1,675 @@
defmodule HttpRequestMock do
require Logger
def request(
%Tesla.Env{
url: url,
method: method,
headers: headers,
query: query,
body: body
} = _env
) do
with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do
res
else
{_, r} = error ->
# Logger.warn(r)
error
end
end
# GET Requests
#
def get(url, query \\ [], body \\ [], headers \\ [])
def get("https://osada.macgirvin.com/channel/mike", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/httpoison_mock/https___osada.macgirvin.com_channel_mike.json")
}}
end
def get(
"https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/mike@osada.macgirvin.com.json")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/29191",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_29191.xml")
}}
end
def get("https://pawoo.net/users/pekorino.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.atom")
}}
end
def get(
"https://pawoo.net/.well-known/webfinger?resource=acct:https://pawoo.net/users/pekorino",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.xml")
}}
end
def get(
"https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/atarifrosch_feed.xml")
}}
end
def get(
"https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=acct:https://social.stopwatchingus-heidelberg.de/user/18330",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/atarifrosch_webfinger.xml")
}}
end
def get("https://mamot.fr/users/Skruyb.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___mamot.fr_users_Skruyb.atom")
}}
end
def get(
"https://mamot.fr/.well-known/webfinger?resource=acct:https://mamot.fr/users/Skruyb",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/skruyb@mamot.fr.atom")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/nonexistant@social.heldscal.la.xml")
}}
end
def get("https://squeet.me/xrd/?uri=lain@squeet.me", _, _,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/lain_squeet.me_webfinger.xml")
}}
end
def get("https://mst3k.interlinked.me/users/luciferMysticus", _, _,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/lucifermysticus.json")
}}
end
def get("https://prismo.news/@mxb", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___prismo.news__mxb.json")
}}
end
def get("https://hubzilla.example.org/channel/kaniini", _, _,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/kaniini@hubzilla.example.org.json")
}}
end
def get("https://niu.moe/users/rye", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/rye.json")
}}
end
def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http___mastodon.example.org_users_admin_status_1234.json"
)
}}
end
def get("https://puckipedia.com/", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/puckipedia.com.json")
}}
end
def get("https://peertube.moe/accounts/7even", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/7even.json")
}}
end
def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/peertube.moe-vid.json")
}}
end
def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-user.json")
}}
end
def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-article.json")
}}
end
def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json")
}}
end
def get("http://mastodon.example.org/@admin/99541947525187367", _, _,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-note-object.json")
}}
end
def get("https://shitposter.club/notice/7369654", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.html")
}}
end
def get("https://mstdn.io/users/mayuutann", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/mayumayu.json")
}}
end
def get("https://mstdn.io/users/mayuutann/statuses/99568293732299394", _, _,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/mayumayupost.json")
}}
end
def get("https://pleroma.soykaf.com/users/lain/feed.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml"
)
}}
end
def get(url, _, _, Accept: "application/xrd+xml,application/jrd+json")
when url in [
"https://pleroma.soykaf.com/.well-known/webfinger?resource=acct:https://pleroma.soykaf.com/users/lain",
"https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/lain"
] do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain.xml")
}}
end
def get("https://shitposter.club/api/statuses/user_timeline/1.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml"
)
}}
end
def get(
"https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___shitposter.club_user_1.xml")
}}
end
def get("https://shitposter.club/notice/2827873", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/httpoison_mock/https___shitposter.club_notice_2827873.html")
}}
end
def get("https://shitposter.club/api/statuses/show/2827873.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml"
)
}}
end
def get("https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b", _, _, _) do
{:ok, %Tesla.Env{status: 200}}
end
def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/spc_5381.atom")
}}
end
def get(
"https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/spc_5381_xrd.xml")
}}
end
def get("http://shitposter.club/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/shitposter.club_host_meta")
}}
end
def get("https://shitposter.club/api/statuses/show/7369654.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.atom")
}}
end
def get("https://shitposter.club/notice/4027863", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.html")
}}
end
def get("https://social.sakamoto.gq/users/eal/feed.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/sakamoto_eal_feed.atom")
}}
end
def get("http://social.sakamoto.gq/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/social.sakamoto.gq_host_meta")
}}
end
def get(
"https://social.sakamoto.gq/.well-known/webfinger?resource=https://social.sakamoto.gq/users/eal",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/eal_sakamoto.xml")
}}
end
def get("https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056", _, _,
Accept: "application/atom+xml"
) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/httpoison_mock/sakamoto.atom")}}
end
def get("http://mastodon.social/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/mastodon.social_host_meta")
}}
end
def get(
"https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/lambadalambda",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.xml"
)
}}
end
def get("http://gs.example.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/gs.example.org_host_meta")
}}
end
def get(
"http://gs.example.org/.well-known/webfinger?resource=http://gs.example.org:4040/index.php/user/1",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http___gs.example.org_4040_index.php_user_1.xml"
)
}}
end
def get("http://gs.example.org/index.php/api/statuses/user_timeline/1.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml"
)
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml"
)
}}
end
def get("http://squeet.me/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/httpoison_mock/squeet.me_host_meta")}}
end
def get("https://squeet.me/xrd?uri=lain@squeet.me", _, _,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/lain_squeet.me_webfinger.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/shp@social.heldscal.la.xml")
}}
end
def get("http://framatube.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/framatube.org_host_meta")
}}
end
def get("http://framatube.org/main/xrd?uri=framasoft@framatube.org", _, _,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/json"}],
body: File.read!("test/fixtures/httpoison_mock/framasoft@framatube.org.json")
}}
end
def get("http://gnusocial.de/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/gnusocial.de_host_meta")
}}
end
def get("http://gnusocial.de/main/xrd?uri=winterdienst@gnusocial.de", _, _,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/winterdienst_webfinger.json")
}}
end
def get("http://status.alpicola.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/status.alpicola.com_host_meta")
}}
end
def get("http://macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/macgirvin.com_host_meta")
}}
end
def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/gerzilla.de_host_meta")
}}
end
def get("https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de", _, _,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/json"}],
body: File.read!("test/fixtures/httpoison_mock/kaniini@gerzilla.de.json")
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml"
)
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/23211",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_23211.xml")
}}
end
def get("http://social.heldscal.la/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/social.heldscal.la_host_meta")
}}
end
def get("https://social.heldscal.la/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/httpoison_mock/social.heldscal.la_host_meta")
}}
end
def get("https://mastodon.social/users/lambadalambda.atom", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.atom")}}
end
def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do
{:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
end
def get(url, query, body, headers) do
{:error,
"Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers)
}"}
end
# POST Requests
#
def post(url, query \\ [], body \\ [], headers \\ [])
def post("http://example.org/needs_refresh", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: ""
}}
end
def post(url, _query, _body, _headers) do
{:error, "Not implemented the mock response for post #{inspect(url)}"}
end
end

View file

@ -1,881 +0,0 @@
defmodule HTTPoisonMock do
alias HTTPoison.Response
def get(url, body \\ [], headers \\ [])
def get("https://prismo.news/@mxb", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___prismo.news__mxb.json")
}}
end
def get("https://osada.macgirvin.com/channel/mike", _, _) do
{:ok,
%Response{
status_code: 200,
body:
File.read!("test/fixtures/httpoison_mock/https___osada.macgirvin.com_channel_mike.json")
}}
end
def get(
"https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com",
_,
_
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mike@osada.macgirvin.com.json")
}}
end
def get("https://info.pleroma.site/activity.json", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity.json")
}}
end
def get("https://info.pleroma.site/activity2.json", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity2.json")
}}
end
def get("https://info.pleroma.site/activity3.json", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity3.json")
}}
end
def get("https://info.pleroma.site/activity4.json", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity4.json")
}}
end
def get("https://info.pleroma.site/actor.json", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___info.pleroma.site_actor.json")
}}
end
def get("https://puckipedia.com/", [Accept: "application/activity+json"], _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/puckipedia.com.json")
}}
end
def get(
"https://gerzilla.de/.well-known/webfinger?resource=acct:kaniini@gerzilla.de",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/kaniini@gerzilla.de.json")
}}
end
def get(
"https://framatube.org/.well-known/webfinger?resource=acct:framasoft@framatube.org",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/framasoft@framatube.org.json")
}}
end
def get(
"https://gnusocial.de/.well-known/webfinger?resource=acct:winterdienst@gnusocial.de",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/winterdienst_webfinger.json")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "nonexistant@social.heldscal.la"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 500,
body: File.read!("test/fixtures/httpoison_mock/nonexistant@social.heldscal.la.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/shp@social.heldscal.la.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "shp@social.heldscal.la"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/shp@social.heldscal.la.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://social.heldscal.la/user/23211"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_23211.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/23211",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_23211.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://social.heldscal.la/user/29191"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_29191.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/29191",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_29191.xml")
}}
end
def get(
"https://mastodon.social/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://mastodon.social/users/lambadalambda"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.xml"
)
}}
end
def get(
"https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/lambadalambda",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.xml"
)
}}
end
def get(
"https://shitposter.club/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://shitposter.club/user/1"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___shitposter.club_user_1.xml")
}}
end
def get(
"https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___shitposter.club_user_1.xml")
}}
end
def get(
"https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/spc_5381_xrd.xml")
}}
end
def get(
"http://gs.example.org/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "http://gs.example.org:4040/index.php/user/1"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http___gs.example.org_4040_index.php_user_1.xml"
)
}}
end
def get(
"http://gs.example.org/.well-known/webfinger?resource=http://gs.example.org:4040/index.php/user/1",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http___gs.example.org_4040_index.php_user_1.xml"
)
}}
end
def get(
"https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=https://social.stopwatchingus-heidelberg.de/user/18330",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/atarifrosch_webfinger.xml")
}}
end
def get(
"https://pleroma.soykaf.com/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://pleroma.soykaf.com/users/lain"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain.xml")
}}
end
def get(
"https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/lain",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain.xml")
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml"
)
}}
end
def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/spc_5381.atom")
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml"
)
}}
end
def get("https://mastodon.social/users/lambadalambda.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.atom"
)
}}
end
def get(
"https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom",
_body,
_headers
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/atarifrosch_feed.xml")
}}
end
def get("https://pleroma.soykaf.com/users/lain/feed.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml"
)
}}
end
def get("https://social.sakamoto.gq/users/eal/feed.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/sakamoto_eal_feed.atom")
}}
end
def get("http://gs.example.org/index.php/api/statuses/user_timeline/1.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml"
)
}}
end
def get("https://shitposter.club/notice/2827873", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!("test/fixtures/httpoison_mock/https___shitposter.club_notice_2827873.html")
}}
end
def get("https://shitposter.club/api/statuses/show/2827873.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml"
)
}}
end
def get("https://shitposter.club/api/statuses/user_timeline/1.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml"
)
}}
end
def post(
"https://social.heldscal.la/main/push/hub",
{:form, _data},
"Content-type": "application/x-www-form-urlencoded"
) do
{:ok,
%Response{
status_code: 202
}}
end
def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _, _) do
{:ok,
%Response{
status_code: 200,
body:
File.read!(
"test/fixtures/httpoison_mock/http___mastodon.example.org_users_admin_status_1234.json"
)
}}
end
def get(
"https://pawoo.net/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://pawoo.net/users/pekorino"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.xml")
}}
end
def get(
"https://pawoo.net/.well-known/webfinger?resource=https://pawoo.net/users/pekorino",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.xml")
}}
end
def get("https://pawoo.net/users/pekorino.atom", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.atom")
}}
end
def get(
"https://mamot.fr/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://mamot.fr/users/Skruyb"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/skruyb@mamot.fr.atom")
}}
end
def get(
"https://mamot.fr/.well-known/webfinger?resource=https://mamot.fr/users/Skruyb",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/skruyb@mamot.fr.atom")
}}
end
def get(
"https://social.sakamoto.gq/.well-known/webfinger",
[Accept: "application/xrd+xml,application/jrd+json"],
params: [resource: "https://social.sakamoto.gq/users/eal"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/eal_sakamoto.xml")
}}
end
def get(
"https://social.sakamoto.gq/.well-known/webfinger?resource=https://social.sakamoto.gq/users/eal",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/eal_sakamoto.xml")
}}
end
def get(
"https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/shp",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/shp@pleroma.soykaf.com.webfigner")
}}
end
def get(
"https://squeet.me/xrd/?uri=lain@squeet.me",
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/lain_squeet.me_webfinger.xml")
}}
end
def get("https://mamot.fr/users/Skruyb.atom", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/https___mamot.fr_users_Skruyb.atom")
}}
end
def get(
"https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056",
[Accept: "application/atom+xml"],
_
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/sakamoto.atom")
}}
end
def get("https://pleroma.soykaf.com/users/shp/feed.atom", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/shp@pleroma.soykaf.com.feed")
}}
end
def get("http://social.heldscal.la/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/social.heldscal.la_host_meta")
}}
end
def get("http://status.alpicola.com/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/status.alpicola.com_host_meta")
}}
end
def get("http://macgirvin.com/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/macgirvin.com_host_meta")
}}
end
def get("http://mastodon.social/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mastodon.social_host_meta")
}}
end
def get("http://shitposter.club/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/shitposter.club_host_meta")
}}
end
def get("http://pleroma.soykaf.com/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/pleroma.soykaf.com_host_meta")
}}
end
def get("http://social.sakamoto.gq/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/social.sakamoto.gq_host_meta")
}}
end
def get("http://gs.example.org/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/gs.example.org_host_meta")
}}
end
def get("http://pawoo.net/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/pawoo.net_host_meta")
}}
end
def get("http://mamot.fr/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mamot.fr_host_meta")
}}
end
def get("http://mastodon.xyz/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mastodon.xyz_host_meta")
}}
end
def get("http://social.wxcafe.net/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/social.wxcafe.net_host_meta")
}}
end
def get("http://squeet.me/.well-known/host-meta", [], follow_redirect: true) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/squeet.me_host_meta")
}}
end
def get(
"http://social.stopwatchingus-heidelberg.de/.well-known/host-meta",
[],
follow_redirect: true
) do
{:ok,
%Response{
status_code: 200,
body:
File.read!("test/fixtures/httpoison_mock/social.stopwatchingus-heidelberg.de_host_meta")
}}
end
def get("http://mastodon.example.org/users/admin", [Accept: "application/activity+json"], _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json")
}}
end
def get(
"https://hubzilla.example.org/channel/kaniini",
[Accept: "application/activity+json"],
_
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/kaniini@hubzilla.example.org.json")
}}
end
def get("https://masto.quad.moe/users/_HellPie", [Accept: "application/activity+json"], _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/hellpie.json")
}}
end
def get("https://niu.moe/users/rye", [Accept: "application/activity+json"], _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/rye.json")
}}
end
def get("https://n1u.moe/users/rye", [Accept: "application/activity+json"], _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/rye.json")
}}
end
def get(
"https://mst3k.interlinked.me/users/luciferMysticus",
[Accept: "application/activity+json"],
_
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/lucifermysticus.json")
}}
end
def get("https://mstdn.io/users/mayuutann", [Accept: "application/activity+json"], _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mayumayu.json")
}}
end
def get(
"http://mastodon.example.org/@admin/99541947525187367",
[Accept: "application/activity+json"],
_
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/mastodon-note-object.json")
}}
end
def get(
"https://mstdn.io/users/mayuutann/statuses/99568293732299394",
[Accept: "application/activity+json"],
_
) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/mayumayupost.json")
}}
end
def get("https://shitposter.club/notice/7369654", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.html")
}}
end
def get("https://shitposter.club/api/statuses/show/7369654.atom", _body, _headers) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/7369654.atom")
}}
end
def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-article.json")
}}
end
def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-user.json")
}}
end
def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/peertube.moe-vid.json")
}}
end
def get("https://peertube.moe/accounts/7even", _, _) do
{:ok,
%Response{
status_code: 200,
body: File.read!("test/fixtures/httpoison_mock/7even.json")
}}
end
def get(url, body, headers) do
{:error,
"Not implemented the mock response for get #{inspect(url)}, #{inspect(body)}, #{
inspect(headers)
}"}
end
def post(url, _body, _headers) do
{:error, "Not implemented the mock response for post #{inspect(url)}"}
end
def post(url, _body, _headers, _options) do
{:error, "Not implemented the mock response for post #{inspect(url)}"}
end
end

View file

@ -2,7 +2,43 @@ defmodule Pleroma.UploadTest do
alias Pleroma.Upload alias Pleroma.Upload
use Pleroma.DataCase use Pleroma.DataCase
describe "Storing a file" do describe "Storing a file with the Local uploader" do
setup [:ensure_local_uploader]
test "returns a media url" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file)
assert %{"url" => [%{"href" => url}]} = data
assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/")
end
test "returns a media url with configured base_url" do
base_url = "https://cache.pleroma.social"
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file, base_url: base_url)
assert %{"url" => [%{"href" => url}]} = data
assert String.starts_with?(url, base_url <> "/media/")
end
test "copies the file to the configured folder with deduping" do test "copies the file to the configured folder with deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
@ -12,10 +48,11 @@ test "copies the file to the configured folder with deduping" do
filename: "an [image.jpg" filename: "an [image.jpg"
} }
data = Upload.store(file, true) {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
assert data["name"] == assert List.first(data["url"])["href"] ==
"e7a6d0cf595bff76f14c9a98b6c199539559e8b844e02e51e5efcfd1f614a2df.jpeg" Pleroma.Web.base_url() <>
"/media/e7a6d0cf595bff76f14c9a98b6c199539559e8b844e02e51e5efcfd1f614a2df.jpg"
end end
test "copies the file to the configured folder without deduping" do test "copies the file to the configured folder without deduping" do
@ -27,7 +64,7 @@ test "copies the file to the configured folder without deduping" do
filename: "an [image.jpg" filename: "an [image.jpg"
} }
data = Upload.store(file, false) {:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data["name"] == "an [image.jpg"
end end
@ -40,7 +77,7 @@ test "fixes incorrect content type" do
filename: "an [image.jpg" filename: "an [image.jpg"
} }
data = Upload.store(file, true) {:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
assert hd(data["url"])["mediaType"] == "image/jpeg" assert hd(data["url"])["mediaType"] == "image/jpeg"
end end
@ -53,7 +90,7 @@ test "adds missing extension" do
filename: "an [image" filename: "an [image"
} }
data = Upload.store(file, false) {:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data["name"] == "an [image.jpg"
end end
@ -66,7 +103,7 @@ test "fixes incorrect file extension" do
filename: "an [image.blah" filename: "an [image.blah"
} }
data = Upload.store(file, false) {:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data["name"] == "an [image.jpg"
end end
@ -79,8 +116,22 @@ test "don't modify filename of an unknown type" do
filename: "test.txt" filename: "test.txt"
} }
data = Upload.store(file, false) {:ok, data} = Upload.store(file)
assert data["name"] == "test.txt" assert data["name"] == "test.txt"
end end
test "copies the file to the configured folder with anonymizing filename" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename])
refute data["name"] == "an [image.jpg"
end
end end
end end

View file

@ -9,6 +9,11 @@ defmodule Pleroma.UserTest do
import Pleroma.Factory import Pleroma.Factory
import Ecto.Query import Ecto.Query
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "ap_id returns the activity pub id for the user" do test "ap_id returns the activity pub id for the user" do
user = UserBuilder.build() user = UserBuilder.build()
@ -34,14 +39,14 @@ test "follow takes a user and another user" do
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
followed = User.get_by_ap_id(followed.ap_id) followed = User.get_by_ap_id(followed.ap_id)
assert followed.info["follower_count"] == 1 assert followed.info.follower_count == 1
assert User.ap_followers(followed) in user.following assert User.ap_followers(followed) in user.following
end end
test "can't follow a deactivated users" do test "can't follow a deactivated users" do
user = insert(:user) user = insert(:user)
followed = insert(:user, info: %{"deactivated" => true}) followed = insert(:user, info: %{deactivated: true})
{:error, _} = User.follow(user, followed) {:error, _} = User.follow(user, followed)
end end
@ -56,8 +61,8 @@ test "can't follow a user who blocked us" do
end end
test "local users do not automatically follow local locked accounts" do test "local users do not automatically follow local locked accounts" do
follower = insert(:user, info: %{"locked" => true}) follower = insert(:user, info: %{locked: true})
followed = insert(:user, info: %{"locked" => true}) followed = insert(:user, info: %{locked: true})
{:ok, follower} = User.maybe_direct_follow(follower, followed) {:ok, follower} = User.maybe_direct_follow(follower, followed)
@ -144,6 +149,18 @@ test "it sets the password_hash, ap_id and following fields" do
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
test "it ensures info is not nil" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} =
changeset
|> Repo.insert()
refute is_nil(user.info)
end
end end
describe "fetching a user from nickname or trying to build one" do describe "fetching a user from nickname or trying to build one" do
@ -185,12 +202,14 @@ test "updates an existing user, if stale" do
local: false, local: false,
nickname: "admin@mastodon.example.org", nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/admin", ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: a_week_ago last_refreshed_at: a_week_ago,
info: %{}
) )
assert orig_user.last_refreshed_at == a_week_ago assert orig_user.last_refreshed_at == a_week_ago
user = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") user = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
assert user.info.source_data["endpoints"]
refute user.last_refreshed_at == orig_user.last_refreshed_at refute user.last_refreshed_at == orig_user.last_refreshed_at
end end
@ -311,45 +330,45 @@ test "it sets the info->note_count property" do
user = User.get_by_ap_id(note.data["actor"]) user = User.get_by_ap_id(note.data["actor"])
assert user.info["note_count"] == nil assert user.info.note_count == 0
{:ok, user} = User.update_note_count(user) {:ok, user} = User.update_note_count(user)
assert user.info["note_count"] == 1 assert user.info.note_count == 1
end end
test "it increases the info->note_count property" do test "it increases the info->note_count property" do
note = insert(:note) note = insert(:note)
user = User.get_by_ap_id(note.data["actor"]) user = User.get_by_ap_id(note.data["actor"])
assert user.info["note_count"] == nil assert user.info.note_count == 0
{:ok, user} = User.increase_note_count(user) {:ok, user} = User.increase_note_count(user)
assert user.info["note_count"] == 1 assert user.info.note_count == 1
{:ok, user} = User.increase_note_count(user) {:ok, user} = User.increase_note_count(user)
assert user.info["note_count"] == 2 assert user.info.note_count == 2
end end
test "it decreases the info->note_count property" do test "it decreases the info->note_count property" do
note = insert(:note) note = insert(:note)
user = User.get_by_ap_id(note.data["actor"]) user = User.get_by_ap_id(note.data["actor"])
assert user.info["note_count"] == nil assert user.info.note_count == 0
{:ok, user} = User.increase_note_count(user) {:ok, user} = User.increase_note_count(user)
assert user.info["note_count"] == 1 assert user.info.note_count == 1
{:ok, user} = User.decrease_note_count(user) {:ok, user} = User.decrease_note_count(user)
assert user.info["note_count"] == 0 assert user.info.note_count == 0
{:ok, user} = User.decrease_note_count(user) {:ok, user} = User.decrease_note_count(user)
assert user.info["note_count"] == 0 assert user.info.note_count == 0
end end
test "it sets the info->follower_count property" do test "it sets the info->follower_count property" do
@ -358,11 +377,11 @@ test "it sets the info->follower_count property" do
User.follow(follower, user) User.follow(follower, user)
assert user.info["follower_count"] == nil assert user.info.follower_count == 0
{:ok, user} = User.update_follower_count(user) {:ok, user} = User.update_follower_count(user)
assert user.info["follower_count"] == 1 assert user.info.follower_count == 1
end end
end end
@ -489,11 +508,11 @@ test "get recipients from activity" do
test ".deactivate can de-activate then re-activate a user" do test ".deactivate can de-activate then re-activate a user" do
user = insert(:user) user = insert(:user)
assert false == !!user.info["deactivated"] assert false == user.info.deactivated
{:ok, user} = User.deactivate(user) {:ok, user} = User.deactivate(user)
assert true == user.info["deactivated"] assert true == user.info.deactivated
{:ok, user} = User.deactivate(user, false) {:ok, user} = User.deactivate(user, false)
assert false == !!user.info["deactivated"] assert false == user.info.deactivated
end end
test ".delete deactivates a user, all follow relationships and all create activities" do test ".delete deactivates a user, all follow relationships and all create activities" do
@ -517,7 +536,7 @@ test ".delete deactivates a user, all follow relationships and all create activi
follower = Repo.get(User, follower.id) follower = Repo.get(User, follower.id)
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
assert user.info["deactivated"] assert user.info.deactivated
refute User.following?(user, followed) refute User.following?(user, followed)
refute User.following?(followed, follower) refute User.following?(followed, follower)
@ -546,7 +565,7 @@ test "html_filter_policy returns nil when rich-text is enabled" do
end end
test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do
user = insert(:user, %{info: %{"no_rich_text" => true}}) user = insert(:user, %{info: %{no_rich_text: true}})
assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user)
end end

View file

@ -5,6 +5,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
alias Pleroma.Activity alias Pleroma.Activity
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "/relay" do describe "/relay" do
test "with the relay active, it returns the relay user", %{conn: conn} do test "with the relay active, it returns the relay user", %{conn: conn} do
res = res =
@ -145,6 +150,20 @@ test "it returns the followers in a collection", %{conn: conn} do
assert result["first"]["orderedItems"] == [user.ap_id] assert result["first"]["orderedItems"] == [user.ap_id]
end end
test "it returns returns empty if the user has 'hide_network' set", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{info: %{hide_network: true}})
User.follow(user, user_two)
result =
conn
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == []
assert result["totalItems"] == 1
end
test "it works for more than 10 users", %{conn: conn} do test "it works for more than 10 users", %{conn: conn} do
user = insert(:user) user = insert(:user)
@ -186,6 +205,20 @@ test "it returns the following in a collection", %{conn: conn} do
assert result["first"]["orderedItems"] == [user_two.ap_id] assert result["first"]["orderedItems"] == [user_two.ap_id]
end end
test "it returns returns empty if the user has 'hide_network' set", %{conn: conn} do
user = insert(:user, %{info: %{hide_network: true}})
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
assert result["totalItems"] == 1
end
test "it works for more than 10 users", %{conn: conn} do test "it works for more than 10 users", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -7,6 +7,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Builders.ActivityBuilder alias Pleroma.Builders.ActivityBuilder
import Pleroma.Factory import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "building a user from his ap id" do describe "building a user from his ap id" do
test "it returns a user" do test "it returns a user" do
@ -14,8 +20,8 @@ test "it returns a user" do
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id) {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org" assert user.nickname == "admin@mastodon.example.org"
assert user.info["source_data"] assert user.info.source_data
assert user.info["ap_enabled"] assert user.info.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers" assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end end
end end

View file

@ -12,6 +12,11 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "handle_incoming" do describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity) activity = insert(:note_activity)
@ -92,7 +97,7 @@ test "it works for incoming notices" do
user = User.get_by_ap_id(object["actor"]) user = User.get_by_ap_id(object["actor"])
assert user.info["note_count"] == 1 assert user.info.note_count == 1
end end
test "it works for incoming notices with hashtags" do test "it works for incoming notices with hashtags" do
@ -307,7 +312,7 @@ test "it works for incoming update activities" do
} }
] ]
assert user.info["banner"]["url"] == [ assert user.info.banner["url"] == [
%{ %{
"href" => "href" =>
"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
@ -337,7 +342,7 @@ test "it works for incoming update activities which lock the account" do
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"]) user = User.get_cached_by_ap_id(data["actor"])
assert user.info["locked"] == true assert user.info.locked == true
end end
test "it works for incoming deletes" do test "it works for incoming deletes" do
@ -543,7 +548,7 @@ test "it works for incoming accepts which were pre-accepted" do
test "it works for incoming accepts which were orphaned" do test "it works for incoming accepts which were orphaned" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:user, %{info: %User.Info{locked: true}})
{:ok, follow_activity} = ActivityPub.follow(follower, followed) {:ok, follow_activity} = ActivityPub.follow(follower, followed)
@ -565,7 +570,7 @@ test "it works for incoming accepts which were orphaned" do
test "it works for incoming accepts which are referenced by IRI only" do test "it works for incoming accepts which are referenced by IRI only" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:user, %{info: %User.Info{locked: true}})
{:ok, follow_activity} = ActivityPub.follow(follower, followed) {:ok, follow_activity} = ActivityPub.follow(follower, followed)
@ -585,7 +590,7 @@ test "it works for incoming accepts which are referenced by IRI only" do
test "it fails for incoming accepts which cannot be correlated" do test "it fails for incoming accepts which cannot be correlated" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:user, %{info: %User.Info{locked: true}})
accept_data = accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json") File.read!("test/fixtures/mastodon-accept-activity.json")
@ -604,7 +609,7 @@ test "it fails for incoming accepts which cannot be correlated" do
test "it fails for incoming rejects which cannot be correlated" do test "it fails for incoming rejects which cannot be correlated" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:user, %{info: %User.Info{locked: true}})
accept_data = accept_data =
File.read!("test/fixtures/mastodon-reject-activity.json") File.read!("test/fixtures/mastodon-reject-activity.json")
@ -623,7 +628,7 @@ test "it fails for incoming rejects which cannot be correlated" do
test "it works for incoming rejects which are orphaned" do test "it works for incoming rejects which are orphaned" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:user, %{info: %User.Info{locked: true}})
{:ok, follower} = User.follow(follower, followed) {:ok, follower} = User.follow(follower, followed)
{:ok, _follow_activity} = ActivityPub.follow(follower, followed) {:ok, _follow_activity} = ActivityPub.follow(follower, followed)
@ -648,7 +653,7 @@ test "it works for incoming rejects which are orphaned" do
test "it works for incoming rejects which are referenced by IRI only" do test "it works for incoming rejects which are referenced by IRI only" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user, %{info: %{"locked" => true}}) followed = insert(:user, %{info: %User.Info{locked: true}})
{:ok, follower} = User.follow(follower, followed) {:ok, follower} = User.follow(follower, followed)
{:ok, follow_activity} = ActivityPub.follow(follower, followed) {:ok, follow_activity} = ActivityPub.follow(follower, followed)
@ -815,18 +820,18 @@ test "it upgrades a user to activitypub" do
assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
assert user.info["note_count"] == 1 assert user.info.note_count == 1
{:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
assert user.info["ap_enabled"] assert user.info.ap_enabled
assert user.info["note_count"] == 1 assert user.info.note_count == 1
assert user.follower_address == "https://niu.moe/users/rye/followers" assert user.follower_address == "https://niu.moe/users/rye/followers"
# Wait for the background task # Wait for the background task
:timer.sleep(1000) :timer.sleep(1000)
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
assert user.info["note_count"] == 1 assert user.info.note_count == 1
activity = Repo.get(Activity, activity.id) activity = Repo.get(Activity, activity.id)
assert user.follower_address in activity.recipients assert user.follower_address in activity.recipients
@ -847,7 +852,7 @@ test "it upgrades a user to activitypub" do
"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
} }
] ]
} = user.info["banner"] } = user.info.banner
refute "..." in activity.recipients refute "..." in activity.recipients

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
describe "/api/pleroma/admin/user" do describe "/api/pleroma/admin/user" do
test "Delete" do test "Delete" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
user = insert(:user) user = insert(:user)
conn = conn =
@ -21,7 +21,7 @@ test "Delete" do
end end
test "Create" do test "Create" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
conn = conn =
build_conn() build_conn()
@ -39,7 +39,7 @@ test "Create" do
describe "/api/pleroma/admin/permission_group" do describe "/api/pleroma/admin/permission_group" do
test "GET is giving user_info" do test "GET is giving user_info" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
conn = conn =
build_conn() build_conn()
@ -47,33 +47,30 @@ test "GET is giving user_info" do
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/permission_group/#{admin.nickname}") |> get("/api/pleroma/admin/permission_group/#{admin.nickname}")
assert json_response(conn, 200) == admin.info assert json_response(conn, 200) == %{
"is_admin" => true,
"is_moderator" => false
}
end end
test "/:right POST, can add to a permission group" do test "/:right POST, can add to a permission group" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
user = insert(:user) user = insert(:user)
user_info =
user.info
|> Map.put("is_admin", true)
conn = conn =
build_conn() build_conn()
|> assign(:user, admin) |> assign(:user, admin)
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/permission_group/#{user.nickname}/admin") |> post("/api/pleroma/admin/permission_group/#{user.nickname}/admin")
assert json_response(conn, 200) == user_info assert json_response(conn, 200) == %{
"is_admin" => true
}
end end
test "/:right DELETE, can remove from a permission group" do test "/:right DELETE, can remove from a permission group" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
user = insert(:user, info: %{"is_admin" => true}) user = insert(:user, info: %{is_admin: true})
user_info =
user.info
|> Map.put("is_admin", false)
conn = conn =
build_conn() build_conn()
@ -81,12 +78,14 @@ test "/:right DELETE, can remove from a permission group" do
|> put_req_header("accept", "application/json") |> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/permission_group/#{user.nickname}/admin") |> delete("/api/pleroma/admin/permission_group/#{user.nickname}/admin")
assert json_response(conn, 200) == user_info assert json_response(conn, 200) == %{
"is_admin" => false
}
end end
end end
test "/api/pleroma/admin/invite_token" do test "/api/pleroma/admin/invite_token" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
conn = conn =
build_conn() build_conn()
@ -98,8 +97,8 @@ test "/api/pleroma/admin/invite_token" do
end end
test "/api/pleroma/admin/password_reset" do test "/api/pleroma/admin/password_reset" do
admin = insert(:user, info: %{"is_admin" => true}) admin = insert(:user, info: %{is_admin: true})
user = insert(:user, info: %{"is_admin" => true}) user = insert(:user)
conn = conn =
build_conn() build_conn()

Some files were not shown because too many files have changed in this diff Show more