Merge pull request 'Elixir 1.19 support' (#7758) from gitlab-mr-iid-4381 into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/7758
This commit is contained in:
commit
38bb47b615
55 changed files with 649 additions and 176 deletions
|
|
@ -19,8 +19,8 @@ variables:
|
|||
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
|
||||
|
||||
steps:
|
||||
unit-testing-elixir-1.18:
|
||||
image: elixir:1.18-otp-27-alpine
|
||||
unit-testing-elixir-1.19:
|
||||
image: elixir:1.19-otp-28-alpine
|
||||
entrypoint: *script_file_entrypoint
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
1
changelog.d/elixir-1-19.add
Normal file
1
changelog.d/elixir-1-19.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Implemented ConfigDB configuration blacklist and accompanying Mix task
|
||||
1
changelog.d/elixir-1-19.fix
Normal file
1
changelog.d/elixir-1-19.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fixed Elixir 1.19 warnings
|
||||
1
changelog.d/logger-backends.change
Normal file
1
changelog.d/logger-backends.change
Normal file
|
|
@ -0,0 +1 @@
|
|||
Changed how Logger is configured due to changes in Elixir and added deprecation warnings for the old configuration format
|
||||
|
|
@ -133,13 +133,17 @@ config :pleroma, Pleroma.Web.Endpoint,
|
|||
]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, backends: [:console]
|
||||
# Primary config
|
||||
config :logger, level: :debug
|
||||
|
||||
config :logger, :console,
|
||||
level: :debug,
|
||||
# Console config
|
||||
config :logger, :default_handler, level: :debug
|
||||
|
||||
config :logger, :default_formatter,
|
||||
format: "\n$time $metadata[$level] $message\n",
|
||||
metadata: [:actor, :path, :type, :user]
|
||||
|
||||
# Syslog config
|
||||
config :logger, :ex_syslogger,
|
||||
level: :debug,
|
||||
ident: "pleroma",
|
||||
|
|
@ -962,6 +966,10 @@ config :pleroma, Pleroma.Search.QdrantSearch,
|
|||
vectors: %{size: 384, distance: "Cosine"}
|
||||
}
|
||||
|
||||
config :pleroma, :database_config_blacklist, [
|
||||
{:pleroma, :logger}
|
||||
]
|
||||
|
||||
config :pleroma, :database_config_whitelist, [
|
||||
{:pleroma},
|
||||
{:cors_plug},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Local
|
|||
# different ports.
|
||||
|
||||
# Do not include timestamps in development logs
|
||||
config :logger, Logger.Backends.Console, format: "$metadata[$level] $message\n"
|
||||
config :logger, :default_formatter, format: "$metadata[$level] $message\n"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ config :pleroma, Pleroma.Web.Endpoint,
|
|||
config :phoenix, serve_endpoints: true
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, Logger.Backends.Console, level: :info
|
||||
config :logger, :console, level: :info
|
||||
config :logger, :default_handler, level: :info
|
||||
config :logger, :ex_syslogger, level: :info
|
||||
|
||||
# ## SSL Support
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ config :pleroma, Pleroma.Captcha,
|
|||
method: Pleroma.Captcha.Mock
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, :console,
|
||||
level: :warning,
|
||||
format: "\n[$level] $message\n"
|
||||
config :logger, :default_handler, level: :warning
|
||||
|
||||
config :logger, :default_formatter,
|
||||
format: "\n[$level] $message\n",
|
||||
colors: [enabled: false]
|
||||
|
||||
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||
|
||||
|
|
|
|||
|
|
@ -858,21 +858,23 @@ Web Push Notifications configuration. You can use the mix task `mix web_push.gen
|
|||
* ``private_key``: VAPID private key
|
||||
|
||||
## :logger
|
||||
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog
|
||||
* Logging to console/stdout is done by default, use `{ExSyslogger, :ex_syslogger}` to log to syslog
|
||||
|
||||
An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed:
|
||||
```elixir
|
||||
config :logger,
|
||||
config :pleroma, :logger,
|
||||
backends: [{ExSyslogger, :ex_syslogger}]
|
||||
|
||||
config :logger, default_handler: false
|
||||
|
||||
config :logger, :ex_syslogger,
|
||||
level: :warning
|
||||
```
|
||||
|
||||
Another example, keeping console output and adding the pid to syslog output:
|
||||
```elixir
|
||||
config :logger,
|
||||
backends: [:console, {ExSyslogger, :ex_syslogger}]
|
||||
config :pleroma, :logger,
|
||||
backends: [{ExSyslogger, :ex_syslogger}]
|
||||
|
||||
config :logger, :ex_syslogger,
|
||||
level: :warning,
|
||||
|
|
@ -883,23 +885,22 @@ See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_s
|
|||
|
||||
An example of logging info to local syslog, but debug to console:
|
||||
```elixir
|
||||
config :logger,
|
||||
backends: [ {ExSyslogger, :ex_syslogger}, :console ],
|
||||
level: :info
|
||||
config :pleroma, :logger,
|
||||
backends: [{ExSyslogger, :ex_syslogger}]
|
||||
|
||||
config :logger, :ex_syslogger,
|
||||
level: :info,
|
||||
ident: "pleroma",
|
||||
format: "$metadata[$level] $message"
|
||||
|
||||
config :logger, :console,
|
||||
level: :debug,
|
||||
config :logger, :default_handler,
|
||||
level: :debug
|
||||
|
||||
config :logger, :default_formatter,
|
||||
format: "\n$time $metadata[$level] $message\n",
|
||||
metadata: [:request_id]
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Database options
|
||||
|
||||
### RUM indexing for full text search
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
## Required dependencies
|
||||
|
||||
* PostgreSQL >=11.0
|
||||
* Elixir >=1.15.0 <1.19
|
||||
* Erlang OTP >=23.0.0 (supported: <28)
|
||||
* Elixir >=1.15.0, <1.20
|
||||
* Erlang OTP >=24.0.0 (supported: <29)
|
||||
* git
|
||||
* file / libmagic
|
||||
* gcc or clang
|
||||
|
|
|
|||
|
|
@ -252,40 +252,43 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
if whitelisted_configs in [nil, false] do
|
||||
shell_error("No unwanted settings in ConfigDB. No changes made.")
|
||||
else
|
||||
whitelisted_groups =
|
||||
whitelisted_configs
|
||||
|> Enum.filter(fn
|
||||
{_group} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.map(fn {group} -> group end)
|
||||
|
||||
whitelisted_keys =
|
||||
whitelisted_configs
|
||||
|> Enum.filter(fn
|
||||
{_group, _key} -> true
|
||||
_ -> false
|
||||
end)
|
||||
{whitelisted_groups, whitelisted_keys} = get_filters(whitelisted_configs)
|
||||
|
||||
filtered =
|
||||
from(c in ConfigDB)
|
||||
|> Repo.all()
|
||||
|> Enum.filter(¬_whitelisted?(&1, whitelisted_groups, whitelisted_keys))
|
||||
|
||||
if not Enum.empty?(filtered) do
|
||||
shell_info("The following settings will be removed from ConfigDB:\n")
|
||||
Enum.each(filtered, &dump(&1))
|
||||
do_config_filter(filtered, force)
|
||||
end
|
||||
end
|
||||
|
||||
if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
|
||||
filtered_ids = Enum.map(filtered, fn %{id: id} -> id end)
|
||||
# Removes blacklisted configuration sections
|
||||
def run(["filter_blacklisted" | rest]) do
|
||||
{options, [], []} =
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [force: :boolean],
|
||||
aliases: [f: :force]
|
||||
)
|
||||
|
||||
Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids))
|
||||
else
|
||||
shell_error("No changes made.")
|
||||
end
|
||||
else
|
||||
shell_error("No unwanted settings in ConfigDB. No changes made.")
|
||||
end
|
||||
force = Keyword.get(options, :force, false)
|
||||
|
||||
start_pleroma()
|
||||
|
||||
blacklisted_configs = Pleroma.Config.get(:database_config_blacklist)
|
||||
|
||||
if blacklisted_configs in [nil, false] do
|
||||
shell_error("No unwanted settings in ConfigDB. No changes made.")
|
||||
else
|
||||
{blacklisted_groups, blacklisted_keys} = get_filters(blacklisted_configs)
|
||||
|
||||
filtered =
|
||||
from(c in ConfigDB)
|
||||
|> Repo.all()
|
||||
|> Enum.filter(&blacklisted?(&1, blacklisted_groups, blacklisted_keys))
|
||||
|
||||
do_config_filter(filtered, force)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -490,8 +493,49 @@ defmodule Mix.Tasks.Pleroma.Config do
|
|||
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
|
||||
end
|
||||
|
||||
defp get_filters(config) do
|
||||
groups =
|
||||
config
|
||||
|> Enum.filter(fn
|
||||
{_group} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.map(fn {group} -> group end)
|
||||
|
||||
keys =
|
||||
config
|
||||
|> Enum.filter(fn
|
||||
{_group, _key} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
{groups, keys}
|
||||
end
|
||||
|
||||
defp not_whitelisted?(%{group: group, key: key}, whitelisted_groups, whitelisted_keys) do
|
||||
not Enum.member?(whitelisted_groups, group) and
|
||||
not Enum.member?(whitelisted_keys, {group, key})
|
||||
end
|
||||
|
||||
defp blacklisted?(%{group: group, key: key}, blacklisted_groups, blacklisted_keys) do
|
||||
Enum.member?(blacklisted_groups, group) or
|
||||
Enum.member?(blacklisted_keys, {group, key})
|
||||
end
|
||||
|
||||
defp do_config_filter(filtered, _) when length(filtered) == 0 do
|
||||
shell_error("No unwanted settings in ConfigDB. No changes made.")
|
||||
end
|
||||
|
||||
defp do_config_filter(filtered, force) do
|
||||
shell_info("The following settings will be removed from ConfigDB:\n")
|
||||
Enum.each(filtered, &dump(&1))
|
||||
|
||||
if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
|
||||
filtered_ids = Enum.map(filtered, fn %{id: id} -> id end)
|
||||
|
||||
Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids))
|
||||
else
|
||||
shell_error("No changes made.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ defmodule Pleroma.Application do
|
|||
# every time the application is restarted, so we disable module
|
||||
# conflicts at runtime
|
||||
Code.compiler_options(ignore_module_conflict: true)
|
||||
configure_logger()
|
||||
Pleroma.Telemetry.Logger.attach()
|
||||
Config.Holder.save_default()
|
||||
Pleroma.HTML.compile_scrubbers()
|
||||
|
|
@ -113,6 +114,44 @@ defmodule Pleroma.Application do
|
|||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
def configure_logger do
|
||||
Config.get([:logger, :backends], [])
|
||||
|> Enum.each(fn backend ->
|
||||
case backend_to_logger(backend) do
|
||||
{:ok, logger} ->
|
||||
add_logger(logger)
|
||||
|
||||
{:error, :console_backend} ->
|
||||
Logger.warning(":console is no longer considered a backend and is enabled by default")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_logger(backend) do
|
||||
case LoggerBackends.add(backend) do
|
||||
{:ok, _} ->
|
||||
Logger.debug("Successfully added logger backend: #{inspect(backend)}")
|
||||
|
||||
{:error, :already_present} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to add logger backend #{inspect(backend)}: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp backend_to_logger(:console), do: {:error, :console_backend}
|
||||
|
||||
defp backend_to_logger({:ex_syslogger = backend, name}) do
|
||||
Logger.warning(
|
||||
"Configuration {:#{backend}, :#{name}} is incorrect. Use {ExSyslogger, :#{name}} instead!"
|
||||
)
|
||||
|
||||
{:ok, {ExSyslogger, name}}
|
||||
end
|
||||
|
||||
defp backend_to_logger(backend), do: {:ok, backend}
|
||||
|
||||
def load_custom_modules do
|
||||
dir = Config.get([:modules, :runtime_dir])
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
|
||||
]
|
||||
|
||||
@logger_formatter_config_knobs [:colors, :format, :metadata, :truncate, :utc_log]
|
||||
|
||||
def check_exiftool_filter do
|
||||
filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
|
||||
|
||||
|
|
@ -218,7 +220,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
check_quarantined_instances_tuples(),
|
||||
check_transparency_exclusions_tuples(),
|
||||
check_simple_policy_tuples(),
|
||||
check_exiftool_filter()
|
||||
check_exiftool_filter(),
|
||||
check_deprecated_logger_config()
|
||||
]
|
||||
|> Enum.reduce(:ok, fn
|
||||
:ok, :ok -> :ok
|
||||
|
|
@ -415,4 +418,72 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_deprecated_logger_config(config) do
|
||||
formatter_config = Enum.filter(config, fn {k, _} -> k in @logger_formatter_config_knobs end)
|
||||
formatter = Logger.default_formatter(formatter_config)
|
||||
log_level = Keyword.get(config, :level, nil)
|
||||
|
||||
Logger.debug("""
|
||||
Reconfiguring console Logger with deprecated configuration syntax.
|
||||
Handler configuration:
|
||||
log_level: #{inspect(log_level)}
|
||||
|
||||
Formatter:
|
||||
#{inspect(formatter)}
|
||||
""")
|
||||
|
||||
if Config.get(:env) != :test do
|
||||
if log_level, do: :logger.update_handler_config(:default, :level, log_level)
|
||||
:logger.set_handler_config(:default, :formatter, formatter)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_deprecated_logger_config() :: :ok | :error
|
||||
def check_deprecated_logger_config do
|
||||
backends_config = Application.get_env(:logger, :backends)
|
||||
console_config = Application.get_env(:logger, :console)
|
||||
|
||||
# NOTE: No need to merge the old backends config since it still works.
|
||||
# And new configuration will just add new Logger backends.
|
||||
backend =
|
||||
if backends_config do
|
||||
Logger.warning("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
|
||||
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use:
|
||||
`config :pleroma, :logger, backends: [...]`
|
||||
|
||||
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
|
||||
`config :logger, :default_handler: false`
|
||||
""")
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
console =
|
||||
if console_config do
|
||||
Logger.warning("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring logging to console.
|
||||
`config :logger, :console` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use
|
||||
`config :logger, default_handler` and `config :logger, :default_formatter`.
|
||||
|
||||
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
|
||||
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
|
||||
""")
|
||||
|
||||
merge_deprecated_logger_config(console_config)
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
if backend or console, do: :error, else: :ok
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ defmodule Pleroma.Config.TransferTask do
|
|||
|
||||
started_applications = Application.started_applications()
|
||||
|
||||
reject = [nil, :postgrex]
|
||||
reject = [nil, :postgrex, :logger]
|
||||
|
||||
reject =
|
||||
if restart_pleroma? do
|
||||
|
|
|
|||
|
|
@ -12,13 +12,27 @@ defmodule Pleroma.Docs.Generator do
|
|||
|
||||
@spec list_behaviour_implementations(behaviour :: module()) :: [module()]
|
||||
def list_behaviour_implementations(behaviour) do
|
||||
:code.all_loaded()
|
||||
|> Enum.filter(fn {module, _} ->
|
||||
# :code.all_loaded() no longer works on Elixir 1.19 and newer since modules are lazily loaded.
|
||||
# We can get a complete list of modules by interating over Application.loaded_applications/0,
|
||||
# but that includes every module which would get loaded below, which is very slow.
|
||||
# Hardcode the Pleroma application instead, which is good enough for now.
|
||||
application_modules =
|
||||
case :application.get_key(:pleroma, :modules) do
|
||||
{:ok, modules} -> modules
|
||||
:undefined -> []
|
||||
end
|
||||
|
||||
# At compile time or in tests some modules will be outside of the Pleroma application.
|
||||
loaded_modules = Enum.map(:code.all_loaded(), fn {module, _} -> module end)
|
||||
|
||||
(application_modules ++ loaded_modules)
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(fn module ->
|
||||
Code.ensure_loaded(module)
|
||||
|
||||
# This shouldn't be needed as all modules are expected to have module_info/1,
|
||||
# but in test environments some transient modules `:elixir_compiler_XX`
|
||||
# are loaded for some reason (where XX is a random integer).
|
||||
Code.ensure_loaded(module)
|
||||
|
||||
if function_exported?(module, :module_info, 1) do
|
||||
module.module_info(:attributes)
|
||||
|> Keyword.get_values(:behaviour)
|
||||
|
|
@ -26,7 +40,6 @@ defmodule Pleroma.Docs.Generator do
|
|||
|> Enum.member?(behaviour)
|
||||
end
|
||||
end)
|
||||
|> Enum.map(fn {module, _} -> module end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
|
|||
@moduledoc "Adds expiration to all local Create activities"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(activity) do
|
||||
activity =
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
|
||||
@moduledoc "Prevent followbots from following with a bit of heuristic"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
alias Pleroma.User
|
||||
|
||||
@moduledoc "Prevent followbots from following with a bit of heuristic"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
# XXX: this should become User.normalize_by_ap_id() or similar, really.
|
||||
defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.AntiMentionSpamPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
require Pleroma.Constants
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp user_has_posted?(%User{} = u), do: u.note_count > 0
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
|
||||
require Logger
|
||||
@moduledoc "Drop and log everything received"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Logger
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(activity) do
|
||||
Logger.debug("REJECTING #{inspect(activity)}")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Object.Updater
|
||||
|
|
@ -10,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
|||
|
||||
@moduledoc "Reject or force-unlisted emojis with certain URLs or names"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp config_remove_url do
|
||||
Pleroma.Config.get([:mrf_emoji, :remove_url], [])
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
|
||||
alias Pleroma.Object
|
||||
|
||||
@moduledoc "Ensure a re: is prepended on replies to a post with a Subject"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Object
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
|
||||
|
||||
def history_awareness, do: :auto
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
require Logger
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(activity) do
|
||||
with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]),
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
|
||||
alias Pleroma.User
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@moduledoc "Remove bot posts from federated timeline"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp get_author(url) do
|
||||
with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Config
|
||||
|
|
@ -14,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
|||
Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.
|
||||
"""
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :manual
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
||||
@moduledoc "Block activities with too much mentions (configurable)"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@moduledoc "Block activities with too much mentions (configurable)"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp delist_activity(activity, threshold) when threshold > 0 do
|
||||
follower_collection = User.get_cached_by_ap_id(activity["actor"]).follower_address
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
|||
@moduledoc "Force a quote line into the message content."
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp build_inline_quote(template, url) do
|
||||
quote_line = String.replace(template, "{url}", "<a href=\"#{url}\">#{url}</a>")
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
||||
@moduledoc "Reject or Word-Replace activities with a keyword or regex"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Web.ActivityPub.MRF.Utils
|
||||
|
||||
@moduledoc "Reject or Word-Replace activities with a keyword or regex"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp string_matches?(string, pattern) when is_binary(pattern) do
|
||||
String.contains?(string, pattern)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
|
|||
@moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
|
||||
@moduledoc "Block activities which mention a user"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create"} = activity) do
|
||||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
|||
@moduledoc "Filter local activities which have no content"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
|
|||
@moduledoc "Does nothing (lets the messages go through unmodified)"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(activity) do
|
||||
{:ok, activity}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
|
|||
@moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
|
||||
@moduledoc "Scrub configured hypertext markup"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.HTML
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
|
|||
- `unlist`: Unlist all detected NSFW content? Default: `false`
|
||||
- `reject`: Reject all detected NSFW content (takes precedence)? Default: `false`
|
||||
"""
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Constants
|
||||
alias Pleroma.HTTP
|
||||
|
|
@ -46,7 +48,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
|
|||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@policy :mrf_nsfw_api
|
||||
|
||||
def build_request_url(url) do
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
||||
@moduledoc "Filter activities depending on their age"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@moduledoc "Filter activities depending on their age"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp check_date(%{"object" => %{"published" => published}} = activity) do
|
||||
with %DateTime{} = now <- DateTime.utc_now(),
|
||||
|
|
|
|||
|
|
@ -15,4 +15,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
|
|||
}
|
||||
@callback history_awareness() :: :auto | :manual
|
||||
@optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
# Otherwise Pleroma.Docs.Generator.list_behaviour_implementations fails
|
||||
# since the Elixir compiler doesn't automatically load the module since v1.19.
|
||||
@compile {:autoload, true}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do
|
|||
@moduledoc """
|
||||
QuietReply alters the scope of activities from local users when replying by enforcing them to be "Unlisted" or "Quiet Public". This delivers the activity to all the expected recipients and instances, but it will not be published in the Federated / The Whole Known Network timelines. It will still be published to the Home timelines of the user's followers and visible to anyone who opens the thread.
|
||||
"""
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def history_awareness, do: :auto
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
|
|||
@moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
|
||||
require Pleroma.Constants
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
|
||||
@moduledoc "Rejects non-public (followers-only, direct) activities"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do
|
|||
@moduledoc "Drop remote reports if they don't contain enough information."
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
@moduledoc "Filter activities depending on their origin instance"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.User
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||
@moduledoc "Detect new emojis by their shortcode and steals them"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
@moduledoc "Detect new emojis by their shortcode and steals them"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp lookup_subchain(actor) do
|
||||
with matches <- Config.get([:mrf_subchain, :match_actor]),
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
||||
alias Pleroma.User
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
@moduledoc """
|
||||
Apply policies based on user tags
|
||||
|
||||
|
|
@ -18,6 +16,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
|
|||
- `mrf_tag:disable-remote-subscription`: Reject non-local follow requests
|
||||
- `mrf_tag:disable-any-subscription`: Reject any follow requests
|
||||
"""
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
|
||||
alias Pleroma.Config
|
||||
|
||||
@moduledoc "Accept-list of users from specified instances"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp filter_by_list(activity, []), do: {:ok, activity}
|
||||
|
||||
defp filter_by_list(%{"actor" => actor} = activity, allow_list) do
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
|
||||
@moduledoc "Filter activities which belong to certain activity vocabularies"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
use Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Undo", "object" => object} = activity) do
|
||||
with {:ok, _} <- filter(object) do
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
end
|
||||
|
||||
def descriptions(conn, _params) do
|
||||
descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
|
||||
descriptions =
|
||||
Pleroma.Docs.JSON.compiled_descriptions()
|
||||
|> Enum.filter(&allowed_config?(&1, :whitelist))
|
||||
|> Enum.reject(&allowed_config?(&1, :blacklist))
|
||||
|
||||
json(conn, translate_descriptions(descriptions))
|
||||
end
|
||||
|
|
@ -132,7 +135,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
with :ok <- configurable_from_database() do
|
||||
results =
|
||||
configs
|
||||
|> Enum.filter(&whitelisted_config?/1)
|
||||
|> Enum.filter(&allowed_config?(&1, :whitelist))
|
||||
|> Enum.reject(&allowed_config?(&1, :blacklist))
|
||||
|> Enum.map(fn
|
||||
%{group: group, key: key, delete: true} = params ->
|
||||
ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})
|
||||
|
|
@ -174,27 +178,34 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
end
|
||||
end
|
||||
|
||||
defp whitelisted_config?(":pleroma", ":database_config_whitelist"), do: false
|
||||
defp allowed_config?(":pleroma", ":database_config_whitelist", _), do: true
|
||||
defp allowed_config?(":pleroma", ":database_config_blacklist", _), do: true
|
||||
|
||||
defp whitelisted_config?(group, key) do
|
||||
if whitelisted_configs = Config.get(:database_config_whitelist) do
|
||||
Enum.any?(whitelisted_configs, fn
|
||||
{whitelisted_group} ->
|
||||
group == inspect(whitelisted_group)
|
||||
defp allowed_config?(group, key, mode) do
|
||||
configs =
|
||||
case mode do
|
||||
:whitelist -> Config.get(:database_config_whitelist)
|
||||
:blacklist -> Config.get(:database_config_blacklist)
|
||||
end
|
||||
|
||||
{whitelisted_group, whitelisted_key} ->
|
||||
group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
|
||||
if configs do
|
||||
Enum.any?(configs, fn
|
||||
{allowed_group} ->
|
||||
group == inspect(allowed_group)
|
||||
|
||||
{allowed_group, allowed_key} ->
|
||||
group == inspect(allowed_group) && key == inspect(allowed_key)
|
||||
end)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
defp whitelisted_config?(%{group: group, key: key}) do
|
||||
whitelisted_config?(group, key)
|
||||
defp allowed_config?(%{group: group, key: key}, mode) do
|
||||
allowed_config?(group, key, mode)
|
||||
end
|
||||
|
||||
defp whitelisted_config?(%{group: group} = config) do
|
||||
whitelisted_config?(group, config[:key])
|
||||
defp allowed_config?(%{group: group} = config, mode) do
|
||||
allowed_config?(group, config[:key], mode)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
7
mix.exs
7
mix.exs
|
|
@ -15,6 +15,13 @@ defmodule Pleroma.Mixfile do
|
|||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
test_coverage: [tool: :covertool, summary: true],
|
||||
test_ignore_filters: [
|
||||
"test/credo/check/consistency/file_location.ex",
|
||||
"test/fixtures/config/temp.exported_from_db.secret.exs",
|
||||
"test/fixtures/config/temp.secret.exs",
|
||||
"test/fixtures/modules/good_mrf.ex",
|
||||
"test/fixtures/modules/runtime_module.ex"
|
||||
],
|
||||
# Docs
|
||||
name: "Pleroma",
|
||||
homepage_url: "https://pleroma.social/",
|
||||
|
|
|
|||
|
|
@ -369,5 +369,39 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
MixTask.run(["filter_whitelisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filters blacklisted settings" do
|
||||
clear_config(:database_config_whitelist, [{:pleroma}])
|
||||
clear_config(:database_config_blacklist, [{:pleroma, :logger}, {:blacklisted_group}])
|
||||
|
||||
insert_config_record(:pleroma, :logger, backends: :console)
|
||||
insert_config_record(:web_push_encryption, :vapid_details, a: 1)
|
||||
insert_config_record(:blacklisted_group, :anything, a: 1)
|
||||
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
|
||||
assert [
|
||||
%ConfigDB{group: :pleroma, key: :instance},
|
||||
%ConfigDB{group: :pleroma, key: Pleroma.Captcha},
|
||||
%ConfigDB{group: :pleroma2, key: :key2},
|
||||
%ConfigDB{group: :web_push_encryption, key: :vapid_details}
|
||||
] = config_records()
|
||||
end
|
||||
|
||||
test "filter_blacklisted doesn't crash when blacklist is unset" do
|
||||
clear_config(:database_config_blacklist, nil)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filter_blacklisted doesn't crash when blacklist is disabled" do
|
||||
clear_config(:database_config_blacklist, false)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
39
test/pleroma/application_test.exs
Normal file
39
test/pleroma/application_test.exs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ApplicationTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
# TODO HACK: Temporarily disabled.
|
||||
# Currently causes a bunch of warnings about :warn being deprecated in logs.
|
||||
# ex_syslogger needs to either be updated upstream or forked.
|
||||
describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [{:ex_syslogger, :ex_syslogger}])
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
test "is handled" do
|
||||
log = capture_log(fn -> Pleroma.Application.configure_logger() end)
|
||||
|
||||
assert log =~
|
||||
"Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!"
|
||||
|
||||
assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "config :pleroma, :logger, :backends: [:console]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [:console])
|
||||
end
|
||||
|
||||
test "emits a warning" do
|
||||
assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~
|
||||
":console is no longer considered a backend and is enabled by default"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.DeprecationWarningsTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
|
@ -388,4 +388,62 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
|
|||
end) =~
|
||||
"Your config is using the old namespace for the Shoutbox configuration."
|
||||
end
|
||||
|
||||
describe "check_deprecated_logger_config" do
|
||||
setup do
|
||||
initial_console = Application.get_env(:logger, :console, nil)
|
||||
initial_backends = Application.get_env(:logger, :backends, nil)
|
||||
|
||||
# NOTE: The Logger does not get reconfigured when running in the test env,
|
||||
# the added and on_exit removed settings below should have no effect besides
|
||||
# triggering the deprecation warnings
|
||||
Application.put_env(:logger, :console, level: :all)
|
||||
Application.put_env(:logger, :backends, [:console])
|
||||
|
||||
on_exit(fn ->
|
||||
if initial_console do
|
||||
Application.put_env(:logger, :console, initial_console)
|
||||
else
|
||||
Application.delete_env(:logger, :console)
|
||||
end
|
||||
|
||||
if initial_backends do
|
||||
Application.put_env(:logger, :backends, initial_backends)
|
||||
else
|
||||
Application.delete_env(:logger, :backends)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "warns on deprecated syntax" do
|
||||
log =
|
||||
capture_log(fn ->
|
||||
Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config()
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
|
||||
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use:
|
||||
`config :pleroma, :logger, backends: [...]`
|
||||
|
||||
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
|
||||
`config :logger, :default_handler: false`
|
||||
"""
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring logging to console.
|
||||
`config :logger, :console` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use
|
||||
`config :logger, default_handler` and `config :logger, :default_formatter`.
|
||||
|
||||
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
|
||||
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -194,19 +194,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|
||||
setup do: clear_config(:configurable_from_database, true)
|
||||
|
||||
setup do:
|
||||
clear_config(:database_config_whitelist, [
|
||||
{:pleroma},
|
||||
{:http},
|
||||
{:idna},
|
||||
{:oban},
|
||||
{:tesla},
|
||||
{:ueberauth}
|
||||
])
|
||||
|
||||
test "create new config setting in db", %{conn: conn} do
|
||||
ueberauth = Application.get_env(:ueberauth, Ueberauth)
|
||||
on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
|
||||
aws = Application.get_env(:ex_aws, :s3)
|
||||
on_exit(fn -> Application.put_env(:ex_aws, :s3, aws) end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -215,9 +205,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
configs: [
|
||||
%{group: ":pleroma", key: ":key1", value: "value1"},
|
||||
%{
|
||||
group: ":ueberauth",
|
||||
key: "Ueberauth",
|
||||
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
|
||||
group: ":cors_plug",
|
||||
key: ":key1",
|
||||
value: [%{"tuple" => [":key", "value"]}]
|
||||
},
|
||||
%{
|
||||
group: ":pleroma",
|
||||
|
|
@ -244,9 +234,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
|
||||
},
|
||||
%{
|
||||
group: ":idna",
|
||||
key: ":key5",
|
||||
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
|
||||
group: ":ex_aws",
|
||||
key: ":s3",
|
||||
value: %{"tuple" => ["access_key_id", "aaaa", []]}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
@ -260,10 +250,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
"db" => [":key1"]
|
||||
},
|
||||
%{
|
||||
"group" => ":ueberauth",
|
||||
"key" => "Ueberauth",
|
||||
"value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
|
||||
"db" => [":consumer_secret"]
|
||||
"group" => ":cors_plug",
|
||||
"key" => ":key1",
|
||||
"value" => [%{"tuple" => [":key", "value"]}],
|
||||
"db" => [":key"]
|
||||
},
|
||||
%{
|
||||
"group" => ":pleroma",
|
||||
|
|
@ -293,10 +283,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
"db" => [":key4"]
|
||||
},
|
||||
%{
|
||||
"group" => ":idna",
|
||||
"key" => ":key5",
|
||||
"value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
|
||||
"db" => [":key5"]
|
||||
"group" => ":ex_aws",
|
||||
"key" => ":s3",
|
||||
"value" => %{"tuple" => ["access_key_id", "aaaa", []]},
|
||||
"db" => [":s3"]
|
||||
}
|
||||
],
|
||||
"need_reboot" => false
|
||||
|
|
@ -322,18 +312,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
nested_5: :upload
|
||||
}
|
||||
|
||||
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
|
||||
assert Application.get_env(:cors_plug, :key1) == [key: "value"]
|
||||
assert Application.get_env(:ex_aws, :s3) == {"access_key_id", "aaaa", []}
|
||||
end
|
||||
|
||||
test "save configs setting without explicit key", %{conn: conn} do
|
||||
adapter = Application.get_env(:http, :adapter)
|
||||
send_user_agent = Application.get_env(:http, :send_user_agent)
|
||||
user_agent = Application.get_env(:http, :user_agent)
|
||||
mime = Application.get_env(:mime, :library)
|
||||
prometheus = Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter)
|
||||
web_push = Application.get_env(:web_push_encryption, :vapid_details)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:http, :adapter, adapter)
|
||||
Application.put_env(:http, :send_user_agent, send_user_agent)
|
||||
Application.put_env(:http, :user_agent, user_agent)
|
||||
Application.put_env(:mime, :library, mime)
|
||||
Application.put_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter, prometheus)
|
||||
Application.put_env(:web_push_encryption, :vapid_details, web_push)
|
||||
end)
|
||||
|
||||
conn =
|
||||
|
|
@ -342,19 +333,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{
|
||||
group: ":http",
|
||||
key: ":adapter",
|
||||
group: ":mime",
|
||||
key: ":library",
|
||||
value: [":someval"]
|
||||
},
|
||||
%{
|
||||
group: ":http",
|
||||
key: ":send_user_agent",
|
||||
group: ":prometheus",
|
||||
key: "Pleroma.Web.Endpoint.MetricsExporter",
|
||||
value: true
|
||||
},
|
||||
%{
|
||||
group: ":http",
|
||||
key: ":user_agent",
|
||||
value: [":default"]
|
||||
group: ":web_push_encryption",
|
||||
key: ":vapid_details",
|
||||
value: [":nope"]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
@ -362,30 +353,30 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
assert json_response_and_validate_schema(conn, 200) == %{
|
||||
"configs" => [
|
||||
%{
|
||||
"group" => ":http",
|
||||
"key" => ":adapter",
|
||||
"group" => ":mime",
|
||||
"key" => ":library",
|
||||
"value" => [":someval"],
|
||||
"db" => [":adapter"]
|
||||
"db" => [":library"]
|
||||
},
|
||||
%{
|
||||
"group" => ":http",
|
||||
"key" => ":send_user_agent",
|
||||
"group" => ":prometheus",
|
||||
"key" => "Pleroma.Web.Endpoint.MetricsExporter",
|
||||
"value" => true,
|
||||
"db" => [":send_user_agent"]
|
||||
"db" => ["Pleroma.Web.Endpoint.MetricsExporter"]
|
||||
},
|
||||
%{
|
||||
"group" => ":http",
|
||||
"key" => ":user_agent",
|
||||
"value" => [":default"],
|
||||
"db" => [":user_agent"]
|
||||
"group" => ":web_push_encryption",
|
||||
"key" => ":vapid_details",
|
||||
"value" => [":nope"],
|
||||
"db" => [":vapid_details"]
|
||||
}
|
||||
],
|
||||
"need_reboot" => false
|
||||
}
|
||||
|
||||
assert Application.get_env(:http, :adapter) == [:someval]
|
||||
assert Application.get_env(:http, :send_user_agent) == true
|
||||
assert Application.get_env(:http, :user_agent) == [:default]
|
||||
assert Application.get_env(:mime, :library) == [:someval]
|
||||
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) == true
|
||||
assert Application.get_env(:web_push_encryption, :vapid_details) == [:nope]
|
||||
end
|
||||
|
||||
test "saving config with partial update", %{conn: conn} do
|
||||
|
|
@ -631,17 +622,17 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"}
|
||||
%{group: ":pleroma", key: ":key1", value: "Pleroma.Web.Endpoint"}
|
||||
]
|
||||
})
|
||||
|
||||
assert json_response_and_validate_schema(conn, 200) == %{
|
||||
"configs" => [
|
||||
%{
|
||||
"group" => ":tesla",
|
||||
"key" => ":adapter",
|
||||
"value" => "Tesla.Adapter.Httpc",
|
||||
"db" => [":adapter"]
|
||||
"group" => ":pleroma",
|
||||
"key" => ":key1",
|
||||
"value" => "Pleroma.Web.Endpoint",
|
||||
"db" => [":key1"]
|
||||
}
|
||||
],
|
||||
"need_reboot" => false
|
||||
|
|
@ -653,14 +644,14 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
admin: admin,
|
||||
token: token
|
||||
} do
|
||||
ueberauth = Application.get_env(:ueberauth, Ueberauth)
|
||||
prometheus = Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter)
|
||||
insert(:config, key: :keyaa1)
|
||||
insert(:config, key: :keyaa2)
|
||||
|
||||
config3 =
|
||||
insert(:config,
|
||||
group: :ueberauth,
|
||||
key: Ueberauth
|
||||
group: :prometheus,
|
||||
key: Pleroma.Web.Endpoint.MetricsExporter
|
||||
)
|
||||
|
||||
conn =
|
||||
|
|
@ -693,7 +684,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|
||||
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
|
||||
assert Application.get_env(:pleroma, :keyaa2) == "another_value"
|
||||
assert Application.get_env(:ueberauth, Ueberauth) == config3.value
|
||||
|
||||
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) ==
|
||||
config3.value
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|
|
@ -704,8 +697,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
configs: [
|
||||
%{group: ":pleroma", key: ":keyaa2", delete: true},
|
||||
%{
|
||||
group: ":ueberauth",
|
||||
key: "Ueberauth",
|
||||
group: ":prometheus",
|
||||
key: "Pleroma.Web.Endpoint.MetricsExporter",
|
||||
delete: true
|
||||
}
|
||||
]
|
||||
|
|
@ -716,7 +709,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
"need_reboot" => false
|
||||
}
|
||||
|
||||
assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
|
||||
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) == prometheus
|
||||
refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
|
||||
end
|
||||
|
||||
|
|
@ -1021,8 +1014,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{
|
||||
"group" => ":oban",
|
||||
"key" => ":queues",
|
||||
"group" => ":pleroma",
|
||||
"key" => ":nonexist_oban_queues",
|
||||
"value" => [
|
||||
%{"tuple" => [":federator_incoming", 50]},
|
||||
%{"tuple" => [":federator_outgoing", 50]},
|
||||
|
|
@ -1039,8 +1032,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
assert json_response_and_validate_schema(conn, 200) == %{
|
||||
"configs" => [
|
||||
%{
|
||||
"group" => ":oban",
|
||||
"key" => ":queues",
|
||||
"group" => ":pleroma",
|
||||
"key" => ":nonexist_oban_queues",
|
||||
"value" => [
|
||||
%{"tuple" => [":federator_incoming", 50]},
|
||||
%{"tuple" => [":federator_outgoing", 50]},
|
||||
|
|
@ -1245,6 +1238,57 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist)
|
||||
end
|
||||
|
||||
test "doesn't set keys in the blacklist", %{conn: conn} do
|
||||
clear_config(:database_config_blacklist, [
|
||||
{:pleroma, :logger},
|
||||
{:pleroma, :key2},
|
||||
{:pleroma, Pleroma.Captcha.NotReal},
|
||||
{:blacklisted_group}
|
||||
])
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{group: ":pleroma", key: ":logger", value: "logconf"},
|
||||
%{group: ":pleroma", key: ":key1", value: "value1"},
|
||||
%{group: ":pleroma", key: ":key2", value: "value2"},
|
||||
%{group: ":not_real", key: ":anything", value: "value3"},
|
||||
%{group: ":blacklisted_group", key: ":anything", value: "value4"}
|
||||
]
|
||||
})
|
||||
|
||||
assert Application.get_env(:pleroma, :logger) == nil
|
||||
assert Application.get_env(:pleroma, :key1) == "value1"
|
||||
assert Application.get_env(:pleroma, :key2) == nil
|
||||
assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == nil
|
||||
assert Application.get_env(:blacklisted_group, :anything) == nil
|
||||
end
|
||||
|
||||
test "doesn't allow updating the database_config_blacklist itself", %{conn: conn} do
|
||||
original_blacklist = Pleroma.Config.get(:database_config_blacklist)
|
||||
|
||||
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_blacklist)
|
||||
|
||||
%{"configs" => configs} =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{
|
||||
group: ":pleroma",
|
||||
key: ":database_config_blacklist",
|
||||
value: [%{"tuple" => [":pleroma", ":key1"]}]
|
||||
}
|
||||
]
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert configs == []
|
||||
assert Pleroma.Config.get(:database_config_blacklist) == original_blacklist
|
||||
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_blacklist)
|
||||
end
|
||||
|
||||
test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
|
||||
assert conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|
|
@ -1515,5 +1559,35 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|
||||
assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions())
|
||||
end
|
||||
|
||||
test "filters by database configuration blacklist and whitelist", %{conn: conn} do
|
||||
clear_config(:database_config_whitelist, [
|
||||
{:pleroma, :instance},
|
||||
{:pleroma, :activitypub},
|
||||
{:pleroma, Pleroma.Upload}
|
||||
])
|
||||
|
||||
clear_config(:database_config_blacklist, [
|
||||
{:pleroma, Pleroma.Upload}
|
||||
])
|
||||
|
||||
children =
|
||||
conn
|
||||
|> get("/api/pleroma/admin/config/descriptions")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(children) == 2
|
||||
|
||||
assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 2
|
||||
|
||||
instance = Enum.find(children, fn c -> c["key"] == ":instance" end)
|
||||
assert instance["children"]
|
||||
|
||||
activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end)
|
||||
assert activitypub["children"]
|
||||
|
||||
web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end)
|
||||
refute web_endpoint["children"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue