From 2753285b7722fdb47f0ebb2180e997cf72f65d1a Mon Sep 17 00:00:00 2001 From: Alex S Date: Sun, 29 Sep 2019 11:17:38 +0300 Subject: [PATCH] config editing through database --- config/description.exs | 527 +++++++++++------- docs/API/admin_api.md | 120 ++-- docs/admin/config.md | 59 ++ lib/mix/tasks/pleroma/config.ex | 107 ++-- lib/pleroma/config/transfer_task.ex | 59 +- lib/pleroma/docs/generator.ex | 136 +++-- lib/pleroma/docs/json.ex | 20 +- ...olicy.ex => media_proxy_warming_policy.ex} | 0 .../mrf/{noop_policy.ex => no_op_policy.ex} | 0 ...st_policy.ex => user_allow_list_policy.ex} | 0 .../web/activity_pub/mrf/vocabulary_policy.ex | 3 +- .../web/admin_api/admin_api_controller.ex | 44 +- lib/pleroma/web/admin_api/config.ex | 120 ++-- lib/pleroma/web/router.ex | 1 + test/config/transfer_task_test.exs | 14 +- test/docs/generator_test.exs | 211 +++++++ test/support/factory.ex | 4 +- test/tasks/config_test.exs | 40 +- .../admin_api/admin_api_controller_test.exs | 112 ++-- test/web/admin_api/config_test.exs | 28 +- 20 files changed, 1133 insertions(+), 472 deletions(-) create mode 100644 docs/admin/config.md rename lib/pleroma/web/activity_pub/mrf/{mediaproxy_warming_policy.ex => media_proxy_warming_policy.ex} (100%) rename lib/pleroma/web/activity_pub/mrf/{noop_policy.ex => no_op_policy.ex} (100%) rename lib/pleroma/web/activity_pub/mrf/{user_allowlist_policy.ex => user_allow_list_policy.ex} (100%) create mode 100644 test/docs/generator_test.exs diff --git a/config/description.exs b/config/description.exs index 45e4b43f1..eeb4a6fe9 100644 --- a/config/description.exs +++ b/config/description.exs @@ -23,17 +23,17 @@ key: :uploader, type: :module, description: "Module which will be used for uploads", - suggestions: [ - Generator.uploaders_list() - ] + suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.MDII, Pleroma.Uploaders.S3] }, %{ key: :filters, type: {:list, :module}, description: "List of filter modules for uploads", - suggestions: [ - Generator.filters_list() - ] + suggestions: + Generator.list_modules_in_dir( + "lib/pleroma/upload/filter", + "Elixir.Pleroma.Upload.Filter." + ) }, %{ key: :link_name, @@ -58,7 +58,50 @@ %{ key: :proxy_opts, type: :keyword, - description: "Proxy options, see `Pleroma.ReverseProxy` documentation" + description: "Options for Pleroma.ReverseProxy", + suggestions: [ + redirect_on_failure: false, + max_body_length: 25 * 1_048_576, + http: [ + follow_redirect: true, + pool: :media + ] + ], + children: [ + %{ + key: :redirect_on_failure, + type: :boolean, + description: + "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" + }, + %{ + key: :max_body_length, + type: :integer, + description: + "limits the content length to be approximately the " <> + "specified length. It is validated with the `content-length` header and also verified when proxying" + }, + %{ + key: :http, + type: :keyword, + description: "HTTP options", + children: [ + %{ + key: :adapter, + type: :keyword, + description: "Adapter specific options" + }, + %{ + key: :proxy_url, + label: "Proxy URL", + type: [:string, :tuple], + description: "Proxy URL", + suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}] + } + ] + } + ] } ] }, @@ -131,9 +174,8 @@ description: "List of actions for the mogrify command", suggestions: [ "strip", - ["strip", "auto-orient"], - [{"implode", "1"}], - ["strip", "auto-orient", {"implode", "1"}] + "auto-orient", + {"implode", "1"} ] } ] @@ -151,8 +193,7 @@ "Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original" <> " filename extension by using {extension}, for example custom-file-name.{extension}", suggestions: [ - "custom-file-name.{extension}", - nil + "custom-file-name.{extension}" ] } ] @@ -213,12 +254,14 @@ %{ group: {:subgroup, Swoosh.Adapters.SMTP}, key: :ssl, + label: "SSL", type: :boolean, description: "`Swoosh.Adapters.SMTP` adapter specific setting" }, %{ group: {:subgroup, Swoosh.Adapters.SMTP}, key: :tls, + label: "TLS", type: :atom, description: "`Swoosh.Adapters.SMTP` adapter specific setting", suggestions: [:always, :never, :if_available] @@ -247,12 +290,14 @@ %{ group: {:subgroup, Swoosh.Adapters.SMTP}, key: :no_mx_lookups, + label: "No MX lookups", type: :boolean, description: "`Swoosh.Adapters.SMTP` adapter specific setting" }, %{ group: {:subgroup, Swoosh.Adapters.Sendgrid}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.Sendgrid` adapter specific setting", suggestions: ["my-api-key"] @@ -280,6 +325,7 @@ %{ group: {:subgroup, Swoosh.Adapters.Mandrill}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.Mandrill` adapter specific setting", suggestions: ["my-api-key"] @@ -287,6 +333,7 @@ %{ group: {:subgroup, Swoosh.Adapters.Mailgun}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.Mailgun` adapter specific setting", suggestions: ["my-api-key"] @@ -301,6 +348,7 @@ %{ group: {:subgroup, Swoosh.Adapters.Mailjet}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.Mailjet` adapter specific setting", suggestions: ["my-api-key"] @@ -315,6 +363,7 @@ %{ group: {:subgroup, Swoosh.Adapters.Postmark}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.Postmark` adapter specific setting", suggestions: ["my-api-key"] @@ -322,6 +371,7 @@ %{ group: {:subgroup, Swoosh.Adapters.SparkPost}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.SparkPost` adapter specific setting", suggestions: ["my-api-key"] @@ -336,7 +386,7 @@ %{ group: {:subgroup, Swoosh.Adapters.AmazonSES}, key: :region, - type: {:string}, + type: :string, description: "`Swoosh.Adapters.AmazonSES` adapter specific setting", suggestions: ["us-east-1", "us-east-2"] }, @@ -357,6 +407,7 @@ %{ group: {:subgroup, Swoosh.Adapters.Dyn}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.Dyn` adapter specific setting", suggestions: ["my-api-key"] @@ -370,6 +421,7 @@ %{ group: {:subgroup, Swoosh.Adapters.SocketLabs}, key: :api_key, + label: "API key", type: :string, description: "`Swoosh.Adapters.SocketLabs` adapter specific setting" }, @@ -392,22 +444,20 @@ type: {:list, :string}, description: "List of the scheme part that is considered valid to be an URL", suggestions: [ - [ - "https", - "http", - "dat", - "dweb", - "gopher", - "ipfs", - "ipns", - "irc", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - "xmpp" - ] + "https", + "http", + "dat", + "dweb", + "gopher", + "ipfs", + "ipns", + "irc", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "xmpp" ] } ] @@ -578,7 +628,7 @@ }, %{ key: :federation_publisher_modules, - type: [:list, :module], + type: {:list, :module}, description: "List of modules for federation publishing", suggestions: [ Pleroma.Web.ActivityPub.Publisher @@ -591,12 +641,13 @@ }, %{ key: :rewrite_policy, - type: {:list, :module}, + type: [:module, {:list, :module}], description: "A list of MRF policies enabled", - suggestions: [ - Pleroma.Web.ActivityPub.MRF.NoOpPolicy, - Generator.mrf_list() - ] + suggestions: + Generator.list_modules_in_dir( + "lib/pleroma/web/activity_pub/mrf", + "Elixir.Pleroma.Web.ActivityPub.MRF." + ) }, %{ key: :public, @@ -644,17 +695,19 @@ }, %{ key: :mrf_transparency, + label: "MRF transparency", type: :boolean, description: "Make the content of your Message Rewrite Facility settings public (via nodeinfo)" }, %{ key: :mrf_transparency_exclusions, + label: "MRF transparency exclusions", type: {:list, :string}, description: "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value", suggestions: [ - ["exclusion.com"] + "exclusion.com" ] }, %{ @@ -698,8 +751,7 @@ description: "A message that will be send to a newly registered users as a direct message", suggestions: [ - "Hi, @username! Welcome to the board!", - nil + "Hi, @username! Welcome on board!" ] }, %{ @@ -707,8 +759,7 @@ type: :string, description: "The nickname of the local user that sends the welcome message", suggestions: [ - "lain", - nil + "lain" ] }, %{ @@ -829,7 +880,7 @@ type: [:atom, :tuple, :module], description: "Where logs will be send, :console - send logs to stdout, {ExSyslogger, :ex_syslogger} - to syslog, Quack.Logger - to Slack.", - suggestions: [[:console, {ExSyslogger, :ex_syslogger}, Quack.Logger]] + suggestions: [:console, {ExSyslogger, :ex_syslogger}, Quack.Logger] } ] }, @@ -861,7 +912,7 @@ %{ key: :metadata, type: {:list, :atom}, - suggestions: [[:request_id]] + suggestions: [:request_id] } ] }, @@ -886,7 +937,7 @@ %{ key: :metadata, type: {:list, :atom}, - suggestions: [[:request_id]] + suggestions: [:request_id] } ] }, @@ -931,10 +982,14 @@ group: :pleroma, key: :frontend_configurations, type: :group, - description: "A keyword list that keeps the configuration data for any kind of frontend", + description: + "This form can be used to configure a keyword list that keeps the configuration data for any " <> + "kind of frontend. By default, settings for pleroma_fe and masto_fe are configured. If you want to " <> + "add your own configuration your settings need to be complete as they will override the defaults.", children: [ %{ key: :pleroma_fe, + label: "Pleroma FE", type: :map, description: "Settings for Pleroma FE", suggestions: [ @@ -977,6 +1032,7 @@ }, %{ key: :redirectRootNoLogin, + label: "Redirect root no login", type: :string, description: "relative URL which indicates where to redirect when a user isn't logged in", @@ -984,6 +1040,7 @@ }, %{ key: :redirectRootLogin, + label: "Redirect root login", type: :string, description: "relative URL which indicates where to redirect when a user is logged in", @@ -991,44 +1048,52 @@ }, %{ key: :showInstanceSpecificPanel, + label: "Show instance specific panel", type: :boolean, description: "Whenether to show the instance's specific panel" }, %{ key: :scopeOptionsEnabled, + label: "Scope options enabled", type: :boolean, description: "Enable setting an notice visibility and subject/CW when posting" }, %{ key: :formattingOptionsEnabled, + label: "Formatting options enabled", type: :boolean, description: "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to :instance, allowed_post_formats" }, %{ key: :collapseMessageWithSubject, + label: "Collapse message with subject", type: :boolean, description: "When a message has a subject(aka Content Warning), collapse it by default" }, %{ key: :hidePostStats, + label: "Hide post stats", type: :boolean, description: "Hide notices statistics(repeats, favorites, ...)" }, %{ key: :hideUserStats, + label: "Hide user stats", type: :boolean, description: "Hide profile statistics(posts, posts per day, followers, followings, ...)" }, %{ key: :scopeCopy, + label: "Scope copy", type: :boolean, description: "Copy the scope (private/unlisted/public) in replies to posts by default" }, %{ key: :subjectLineBehavior, + label: "Subject line behavior", type: :string, description: "Allows changing the default behaviour of subject lines in replies. `email`: Copy and preprend re:, as in email, @@ -1038,6 +1103,7 @@ }, %{ key: :alwaysShowSubjectInput, + label: "Always show subject input", type: :boolean, description: "When set to false, auto-hide the subject field when it's empty" } @@ -1045,6 +1111,7 @@ }, %{ key: :masto_fe, + label: "Masto FE", type: :map, description: "Settings for Masto FE", suggestions: [ @@ -1055,6 +1122,7 @@ children: [ %{ key: :showInstanceSpecificPanel, + label: "Show instance specific panel", type: :boolean, description: "Whenether to show the instance's specific panel" } @@ -1071,20 +1139,18 @@ children: [ %{ key: :mascots, - type: :keyword, + type: {:keyword, :map}, description: "Keyword of mascots, each element MUST contain both a url and a mime_type key", suggestions: [ - [ - pleroma_fox_tan: %{ - url: "/images/pleroma-fox-tan-smol.png", - mime_type: "image/png" - }, - pleroma_fox_tan_shy: %{ - url: "/images/pleroma-fox-tan-shy.png", - mime_type: "image/png" - } - ] + pleroma_fox_tan: %{ + url: "/images/pleroma-fox-tan-smol.png", + mime_type: "image/png" + }, + pleroma_fox_tan_shy: %{ + url: "/images/pleroma-fox-tan-shy.png", + mime_type: "image/png" + } ] }, %{ @@ -1140,6 +1206,7 @@ %{ group: :pleroma, key: :mrf_simple, + label: "MRF simple", type: :group, description: "Message Rewrite Facility", children: [ @@ -1151,6 +1218,7 @@ }, %{ key: :media_nsfw, + label: "Media NSFW", type: {:list, :string}, description: "List of instances to put medias as NSFW(sensitive) from", suggestions: ["example.com", "*.example.com"] @@ -1197,6 +1265,7 @@ %{ group: :pleroma, key: :mrf_subchain, + label: "MRF subchain", type: :group, description: "This policy processes messages through an alternate pipeline when a given message matches certain criteria." <> @@ -1217,10 +1286,14 @@ %{ group: :pleroma, key: :mrf_rejectnonpublic, + description: + "MRF RejectNonPublic settings. RejectNonPublic drops posts with non-public visibility settings.", + label: "MRF reject non public", type: :group, children: [ %{ key: :allow_followersonly, + label: "Allow followers-only", type: :boolean, description: "whether to allow followers-only posts" }, @@ -1234,6 +1307,7 @@ %{ group: :pleroma, key: :mrf_hellthread, + label: "MRF hellthread", type: :group, description: "Block messages with too much mentions", children: [ @@ -1257,6 +1331,7 @@ %{ group: :pleroma, key: :mrf_keyword, + label: "MRF keyword", type: :group, description: "Reject or Word-Replace messages with a keyword or regex", children: [ @@ -1276,9 +1351,9 @@ }, %{ key: :replace, - type: [{:string, :string}, {:regex, :string}], + type: [{:tuple, :string, :string}, {:tuple, :regex, :string}], description: - "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression", + "A list of tuples containing {pattern, replacement}, pattern can be a string or a regular expression.", suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}] } ] @@ -1286,6 +1361,7 @@ %{ group: :pleroma, key: :mrf_mention, + label: "MRF mention", type: :group, description: "Block messages which mention a user", children: [ @@ -1293,13 +1369,14 @@ key: :actors, type: {:list, :string}, description: "A list of actors, for which to drop any posts mentioning", - suggestions: [["actor1", "actor2"]] + suggestions: ["actor1", "actor2"] } ] }, %{ group: :pleroma, key: :mrf_vocabulary, + label: "MRF vocabulary", type: :group, description: "Filter messages which belong to certain activity vocabularies", children: [ @@ -1308,14 +1385,14 @@ type: {:list, :string}, description: "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted", - suggestions: [["Create", "Follow", "Mention", "Announce", "Like"]] + suggestions: ["Create", "Follow", "Mention", "Announce", "Like"] }, %{ key: :reject, type: {:list, :string}, description: "A list of ActivityStreams terms to reject. If empty, no messages are rejected", - suggestions: [["Create", "Follow", "Mention", "Announce", "Like"]] + suggestions: ["Create", "Follow", "Mention", "Announce", "Like"] } ] }, @@ -1355,7 +1432,49 @@ key: :proxy_opts, type: :keyword, description: "Options for Pleroma.ReverseProxy", - suggestions: [[max_body_length: 25 * 1_048_576, redirect_on_failure: false]] + suggestions: [ + redirect_on_failure: false, + max_body_length: 25 * 1_048_576, + http: [ + follow_redirect: true, + pool: :media + ] + ], + children: [ + %{ + key: :redirect_on_failure, + type: :boolean, + description: + "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" + }, + %{ + key: :max_body_length, + type: :integer, + description: + "limits the content length to be approximately the " <> + "specified length. It is validated with the `content-length` header and also verified when proxying" + }, + %{ + key: :http, + type: :keyword, + description: "HTTP options", + children: [ + %{ + key: :adapter, + type: :keyword, + description: "Adapter specific options" + }, + %{ + key: :proxy_url, + label: "Proxy URL", + type: [:string, :tuple], + description: "Proxy URL", + suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}] + } + ] + } + ] }, %{ key: :whitelist, @@ -1404,10 +1523,12 @@ children: [ %{ key: :http, - type: :keyword, + label: "HTTP", + type: {:keyword, :integer, :tuple}, description: "http protocol configuration", suggestions: [ - [port: 8080, ip: {127, 0, 0, 1}] + port: 8080, + ip: {127, 0, 0, 1} ], children: [ %{ @@ -1415,21 +1536,20 @@ type: {:list, :tuple}, description: "dispatch settings", suggestions: [ - [ - {:_, - [ - {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, - {"/websocket", Phoenix.Endpoint.CowboyWebSocket, - {Phoenix.Transports.WebSocket, - {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, - {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} - ]} - # end copied from config.exs - ] + {:_, + [ + {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, + {"/websocket", Phoenix.Endpoint.CowboyWebSocket, + {Phoenix.Transports.WebSocket, + {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, + {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}} + ]} + # end copied from config.exs ] }, %{ key: :ip, + label: "IP", type: :tuple, description: "ip", suggestions: [ @@ -1448,10 +1568,13 @@ }, %{ key: :url, - type: :keyword, + label: "URL", + type: {:keyword, :string, :integer}, description: "configuration for generating urls", suggestions: [ - [host: "example.com", port: 2020, scheme: "https"] + host: "example.com", + port: 2020, + scheme: "https" ], children: [ %{ @@ -1504,7 +1627,7 @@ %{ key: :render_errors, type: :keyword, - suggestions: [[view: Pleroma.Web.ErrorView, accepts: ~w(json)]], + suggestions: [view: Pleroma.Web.ErrorView, accepts: ~w(json)], children: [ %{ key: :view, @@ -1521,7 +1644,7 @@ %{ key: :pubsub, type: :keyword, - suggestions: [[name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]], + suggestions: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2], children: [ %{ key: :name, @@ -1588,17 +1711,20 @@ }, %{ key: :sts, + label: "STS", type: :boolean, description: "Whether to additionally send a Strict-Transport-Security header" }, %{ key: :sts_max_age, + label: "STS max age", type: :integer, description: "The maximum age for the Strict-Transport-Security header if sent", suggestions: [31_536_000] }, %{ key: :ct_max_age, + label: "CT max age", type: :integer, description: "The maximum age for the Expect-CT header if sent", suggestions: [2_592_000] @@ -1611,6 +1737,7 @@ }, %{ key: :report_uri, + label: "Report URI", type: :string, description: "Adds the specified url to report-uri and report-to group in CSP header", suggestions: ["https://example.com/report-uri"] @@ -1754,20 +1881,18 @@ }, %{ key: :queues, - type: :keyword, + type: {:keyword, :integer}, description: "Background jobs queues (keys: queues, values: max numbers of concurrent jobs)", suggestions: [ - [ - activity_expiration: 10, - background: 5, - federator_incoming: 50, - federator_outgoing: 50, - mailer: 10, - scheduled_activities: 10, - transmogrifier: 20, - web_push: 50 - ] + activity_expiration: 10, + background: 5, + federator_incoming: 50, + federator_outgoing: 50, + mailer: 10, + scheduled_activities: 10, + transmogrifier: 20, + web_push: 50 ], children: [ %{ @@ -1830,7 +1955,7 @@ children: [ %{ key: :retries, - type: :keyword, + type: {:keyword, :integer}, description: "Max retry attempts for failed jobs, per `Oban` queue", suggestions: [ [ @@ -1845,22 +1970,21 @@ group: :pleroma, key: Pleroma.Web.Metadata, type: :group, - decsription: "Metadata-related settings", + description: "Metadata-related settings", children: [ %{ key: :providers, type: {:list, :module}, description: "List of metadata providers to enable", suggestions: [ - [ - Pleroma.Web.Metadata.Providers.OpenGraph, - Pleroma.Web.Metadata.Providers.TwitterCard, - Pleroma.Web.Metadata.Providers.RelMe - ] + Pleroma.Web.Metadata.Providers.OpenGraph, + Pleroma.Web.Metadata.Providers.TwitterCard, + Pleroma.Web.Metadata.Providers.RelMe ] }, %{ key: :unfurl_nsfw, + label: "Unfurl NSFW", type: :boolean, description: "If set to true nsfw attachments will be shown in previews" } @@ -1870,39 +1994,45 @@ group: :pleroma, key: :rich_media, type: :group, + description: + "If enabled the instance will parse metadata from attached links to generate link previews.", children: [ %{ key: :enabled, type: :boolean, - description: - "if enabled the instance will parse metadata from attached links to generate link previews" + description: "Enables/disables RichMedia." }, %{ key: :ignore_hosts, type: {:list, :string}, - description: "list of hosts which will be ignored by the metadata parser", - suggestions: [["accounts.google.com", "xss.website"]] + description: "List of hosts which will be ignored by the metadata parser.", + suggestions: ["accounts.google.com", "xss.website"] }, %{ key: :ignore_tld, + label: "Ignore TLD", type: {:list, :string}, - description: "list TLDs (top-level domains) which will ignore for parse metadata", - suggestions: [["local", "localdomain", "lan"]] + description: "List TLDs (top-level domains) which will ignore for parse metadata.", + suggestions: ["local", "localdomain", "lan"] }, %{ key: :parsers, type: {:list, :module}, - description: "list of Rich Media parsers", + description: "List of Rich Media parsers.", suggestions: [ - Generator.richmedia_parsers() + Pleroma.Web.RichMedia.Parsers.MetaTagsParser, + Pleroma.Web.RichMedia.Parsers.OEmbed, + Pleroma.Web.RichMedia.Parsers.OGP, + Pleroma.Web.RichMedia.Parsers.TwitterCard ] }, %{ key: :ttl_setters, + label: "TTL setters", type: {:list, :module}, - description: "list of rich media ttl setters", + description: "List of rich media ttl setters.", suggestions: [ - [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl] + Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl ] } ] @@ -2055,23 +2185,57 @@ }, %{ key: :ssl, + label: "SSL", type: :boolean, description: "true to use SSL, usually implies the port 636" }, %{ key: :sslopts, + label: "SSL options", type: :keyword, - description: "additional SSL options" + description: "additional SSL options", + suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer], + children: [ + %{ + key: :cacertfile, + type: :string, + description: "Path to file with PEM encoded cacerts", + suggestions: ["path/to/file/with/PEM/cacerts"] + }, + %{ + key: :verify, + type: :atom, + description: "Type of cert verification", + suggestions: [:verify_peer] + } + ] }, %{ key: :tls, + label: "TLS", type: :boolean, description: "true to start TLS, usually implies the port 389" }, %{ key: :tlsopts, + label: "TLS options", type: :keyword, - description: "additional TLS options" + description: "additional TLS options", + suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer], + children: [ + %{ + key: :cacertfile, + type: :string, + description: "Path to file with PEM encoded cacerts", + suggestions: ["path/to/file/with/PEM/cacerts"] + }, + %{ + key: :verify, + type: :atom, + description: "Type of cert verification", + suggestions: [:verify_peer] + } + ] }, %{ key: :base, @@ -2120,7 +2284,7 @@ }, %{ key: :oauth_consumer_strategies, - type: :string, + type: {:list, :string}, description: "the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <> " Each entry in this space-delimited string should be of format or :" <> @@ -2163,7 +2327,7 @@ }, %{ key: :interval, - type: :ininteger, + type: :integer, description: "Minimum interval between digest emails to one user", suggestions: [7] }, @@ -2185,9 +2349,9 @@ children: [ %{ key: :logo, - type: [:string, nil], + type: :string, description: "a path to a custom logo. Set it to nil to use the default Pleroma logo", - suggestions: ["some/path/logo.png", nil] + suggestions: ["some/path/logo.png"] }, %{ key: :styling, @@ -2279,26 +2443,24 @@ key: :shortcode_globs, type: {:list, :string}, description: "Location of custom emoji files. * can be used as a wildcard", - suggestions: [["/emoji/custom/**/*.png"]] + suggestions: ["/emoji/custom/**/*.png"] }, %{ key: :pack_extensions, type: {:list, :string}, description: "A list of file extensions for emojis, when no emoji.txt for a pack is present", - suggestions: [[".png", ".gif"]] + suggestions: [".png", ".gif"] }, %{ key: :groups, - type: :keyword, + type: {:keyword, :string, {:list, :string}}, description: "Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname" <> " and the value the location or array of locations. * can be used as a wildcard", suggestions: [ - [ - # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md` - Custom: ["/emoji/*.png", "/emoji/**/*.png"] - ] + # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md` + Custom: ["/emoji/*.png", "/emoji/**/*.png"] ] }, %{ @@ -2389,7 +2551,8 @@ group: :esshd, type: :group, description: - "To enable simple command line interface accessible over ssh, add a setting like this to your configuration file", + "Before enabling this you must add :esshd to mix.exs as one of the extra_applications " <> + "and generate host keys in your priv dir with ssh-keygen -m PEM -N \"\" -b 2048 -t rsa -f ssh_host_rsa_key", children: [ %{ key: :enabled, @@ -2443,27 +2606,27 @@ %{ key: "application/xml", type: {:list, :string}, - suggestions: [["xml"]] + suggestions: ["xml"] }, %{ key: "application/xrd+xml", type: {:list, :string}, - suggestions: [["xrd+xml"]] + suggestions: ["xrd+xml"] }, %{ key: "application/jrd+json", type: {:list, :string}, - suggestions: [["jrd+json"]] + suggestions: ["jrd+json"] }, %{ key: "application/activity+json", type: {:list, :string}, - suggestions: [["activity+json"]] + suggestions: ["activity+json"] }, %{ key: "application/ld+json", type: {:list, :string}, - suggestions: [["activity+json"]] + suggestions: ["activity+json"] } ] } @@ -2560,6 +2723,8 @@ %{ group: :pleroma, key: Pleroma.Uploaders.MDII, + description: + "Uploader for https://github.com/hakaba-hitoyo/minimal-digital-image-infrastructure", type: :group, children: [ %{ @@ -2582,8 +2747,10 @@ children: [ %{ key: :proxy_url, - type: [:string, :atom, nil], - suggestions: ["localhost:9020", {:socks5, :localhost, 3090}, nil] + label: "Proxy URL", + type: [:string, :tuple], + description: "Proxy URL", + suggestions: ["localhost:9020", {:socks5, :localhost, 3090}] }, %{ key: :send_user_agent, @@ -2592,16 +2759,8 @@ %{ key: :adapter, type: :keyword, - suggestions: [ - [ - ssl_options: [ - # Workaround for remote server certificate chain issues - partial_chain: &:hackney_connect.partial_chain/1, - # We don't support TLS v1.3 yet - versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"] - ] - ] - ] + description: "Adapter specific options", + suggestions: [] } ] }, @@ -2629,7 +2788,7 @@ %{ key: :scrub_policy, type: {:list, :module}, - suggestions: [[Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]] + suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default] } ] }, @@ -2647,6 +2806,8 @@ %{ group: :pleroma, key: :mrf_normalize_markup, + label: "MRF normalize markup", + description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.", type: :group, children: [ %{ @@ -2665,38 +2826,36 @@ key: :restricted_nicknames, type: {:list, :string}, suggestions: [ - [ - ".well-known", - "~", - "about", - "activities", - "api", - "auth", - "check_password", - "dev", - "friend-requests", - "inbox", - "internal", - "main", - "media", - "nodeinfo", - "notice", - "oauth", - "objects", - "ostatus_subscribe", - "pleroma", - "proxy", - "push", - "registration", - "relay", - "settings", - "status", - "tag", - "user-search", - "user_exists", - "users", - "web" - ] + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "check_password", + "dev", + "friend-requests", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "ostatus_subscribe", + "pleroma", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "user-search", + "user_exists", + "users", + "web" ] } ] @@ -2713,20 +2872,18 @@ %{ key: :methods, type: {:list, :string}, - suggestions: [["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]] + suggestions: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"] }, %{ key: :expose, - type: :string, + type: {:list, :string}, suggestions: [ - [ - "Link", - "X-RateLimit-Reset", - "X-RateLimit-Limit", - "X-RateLimit-Remaining", - "X-Request-Id", - "Idempotency-Key" - ] + "Link", + "X-RateLimit-Reset", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-Request-Id", + "Idempotency-Key" ] }, %{ @@ -2736,7 +2893,7 @@ %{ key: :headers, type: {:list, :string}, - suggestions: [["Authorization", "Content-Type", "Idempotency-Key"]] + suggestions: ["Authorization", "Content-Type", "Idempotency-Key"] } ] }, @@ -2745,16 +2902,14 @@ key: Pleroma.Plugs.RemoteIp, type: :group, description: """ - **If your instance is not behind at least one reverse proxy, you should not enable this plug.** - `Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. + **If your instance is not behind at least one reverse proxy, you should not enable this plug.** """, children: [ %{ key: :enabled, type: :boolean, - description: "Enable/disable the plug. Defaults to `false`.", - suggestions: [true, false] + description: "Enable/disable the plug. Defaults to `false`." }, %{ key: :headers, @@ -2788,7 +2943,7 @@ type: :integer, description: "activity pub routes (except question activities). Defaults to `nil` (no expiration).", - suggestions: [30_000, nil] + suggestions: [30_000] }, %{ key: :activity_pub_question, diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index d98a78af0..851c526d6 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -669,7 +669,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ### Run mix task pleroma.config migrate_to_db -Copy settings on key `:pleroma` to DB. +Copies `pleroma` environment settings to the database. - Params: none - Response: @@ -682,7 +682,7 @@ Copy settings on key `:pleroma` to DB. ### Run mix task pleroma.config migrate_from_db -Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB. +Copies all settings from database to `config/{env}.exported_from_db.secret.exs` with deletion from the table. Where `{env}` is the environment in which `pleroma` is running. - Params: none - Response: @@ -693,9 +693,9 @@ Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with dele ## `GET /api/pleroma/admin/config` -### List config settings +### Get saved config settings -List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. +**Only works when `:dynamic_configuration` is `true`.** - Params: none - Response: @@ -704,9 +704,9 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur { configs: [ { - "group": string, - "key": string or string with leading `:` for atoms, - "value": string or {} or [] or {"tuple": []} + "group": ":pleroma", + "key": "Pleroma.Upload", + "value": [] } ] } @@ -716,44 +716,61 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur ### Update config settings -Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. -Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`. -Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`. -Tuples can be passed as `{"tuple": ["first_val", Pleroma.Module, []]}`. -`{"tuple": ["some_string", "Pleroma.Some.Module", []]}` will be converted to `{"some_string", Pleroma.Some.Module, []}`. -Keywords can be passed as lists with 2 child tuples, e.g. -`[{"tuple": ["first_val", Pleroma.Module]}, {"tuple": ["second_val", true]}]`. +**Only works when `:dynamic_configuration` is `true`.** -If value contains list of settings `[subkey: val1, subkey2: val2, subkey3: val3]`, it's possible to remove only subkeys instead of all settings passing `subkeys` parameter. E.g.: -{"group": "pleroma", "key": "some_key", "delete": "true", "subkeys": [":subkey", ":subkey3"]}. +Some modifications are necessary to save the config settings correctly: -Compile time settings (need instance reboot): -- all settings by this keys: +- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules; +``` +"Pleroma.Upload" -> Pleroma.Upload +"Oban" -> Oban +``` +- strings starting with `:` will be converted to atoms; +``` +":pleroma" -> :pleroma +``` +- objects with `tuple` key and array value will be converted to atoms; +``` +{"tuple": ["string", "Pleroma.Upload", []]} -> {"string", Pleroma.Upload, []} +``` +- arrays with *tuple objects* and 2 childs in array will be converted to keywords; +``` +[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}] -> [key1: "value", key2: "value"] +``` + +Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as: +- all settings inside these keys: - `:hackney_pools` - `:chat` - `Pleroma.Web.Endpoint` - - `Pleroma.Repo` -- part settings: - - `Pleroma.Captcha` -> `:seconds_valid` - - `Pleroma.Upload` -> `:proxy_remote` - - `:instance` -> `:upload_limit` +- partially settings inside these keys: + - `:seconds_valid` in `Pleroma.Captcha` + - `:proxy_remote` in `Pleroma.Upload` + - `:upload_limit` in `:instance` - Params: - - `configs` => [ - - `group` (string) - - `key` (string or string with leading `:` for atoms) - - `value` (string, [], {} or {"tuple": []}) - - `delete` = true (optional, if parameter must be deleted) - - `subkeys` [(string with leading `:` for atoms)] (optional, works only if `delete=true` parameter is passed, otherwise will be ignored) - ] + - `configs` - array of config objects + - config object params: + - `group` - string (**required**) + - `key` - string (**required**) + - `value` - string, [], {} or {"tuple": []} (**required**) + - `delete` - true (*optional*, if setting must be deleted) + - `subkeys` - array of strings (*optional*, only works when `delete=true` parameter is passed, otherwise will be ignored) -- Request (example): +*When a value have several nested settings, you can delete only some nested settings by passing a parameter `subkeys`, without deleting all settings by key.* +``` +[subkey: val1, subkey2: val2, subkey3: val3] \\ initial value +{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]} \\ passing json for deletion +[subkey2: val2] \\ value after deletion +``` + +- Request: ```json { configs: [ { - "group": "pleroma", + "group": ":pleroma", "key": "Pleroma.Upload", "value": [ {"tuple": [":uploader", "Pleroma.Uploaders.Local"]}, @@ -784,14 +801,47 @@ Compile time settings (need instance reboot): { configs: [ { - "group": string, - "key": string or string with leading `:` for atoms, - "value": string or {} or [] or {"tuple": []} + "group": ":pleroma", + "key": "Pleroma.Upload", + "value": [...] } ] } ``` +## ` GET /api/pleroma/admin/config/descriptions` + +### Get JSON with config descriptions. +Loads json generated from `config/descriptions.exs`. + +- Params: none +- Response: + +```json +[{ + "group": ":pleroma", // string + "key": "ModuleName", // string + "type": "group", // string or list with possible values, + "description": "Upload general settings", // string + "children": [ + { + "key": ":uploader", // string or module name `Pleroma.Upload` + "type": "module", + "description": "Module which will be used for uploads", + "suggestions": ["module1", "module2"] + }, + { + "key": ":filters", + "type": ["list", "module"], + "description": "List of filter modules for uploads", + "suggestions": [ + "module1", "module2", "module3" + ] + } + ] +}] +``` + ## `GET /api/pleroma/admin/moderation_log` ### Get moderation log diff --git a/docs/admin/config.md b/docs/admin/config.md new file mode 100644 index 000000000..f42ec8975 --- /dev/null +++ b/docs/admin/config.md @@ -0,0 +1,59 @@ +# Configuring instance +You can configure your instance from admin interface. You need account with admin rights and little change in config file, which will allow settings dynamic configuration from database. + +```elixir +config :pleroma, :instance, + dynamic_configuration: true +``` + +## How it works +Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. These settings are needed in `compile time`, that's why settings are duplicated to the file. + +File with duplicated settings is located in `config/{env}.exported_from_db.exs`. For prod env it will be `config/prod.exported_from_db.exs`. + +## How to set it up +You need to migrate your existing settings to the database. You can do this with mix task (all config files will remain untouched): +```bash +mix pleroma.config migrate_to_db +``` +Now you can change settings in admin interface. After each save, settings are duplicated to the `config/{env}.exported_from_db.exs` file. + +**ATTENTION** + +**Be careful while changing the settings. Every inaccurate configuration change can break the federation or the instance load.** + +*Compile time settings, which require instance reboot and can break instance loading:* +- all settings inside these keys: + - `:hackney_pools` + - `:chat` + - `Pleroma.Web.Endpoint` +- partially settings inside these keys: + - `:seconds_valid` in `Pleroma.Captcha` + - `:proxy_remote` in `Pleroma.Upload` + - `:upload_limit` in `:instance` + +## How to remove it + +1. Truncate or delete all values from `config` table +```sql +TRUNCATE TABLE config; +``` +2. Delete `config/{env}.exported_from_db.exs`. + +For `prod` env: +```bash +cd /opt/pleroma +cp config/prod.exported_from_db.exs config/exported_from_db.back +rm -rf config/prod.exported_from_db.exs +``` +*If you don't want to backup settings, you can skip step with `cp` command.* + +3. Set dynamic configuration to `false`. +```elixir +config :pleroma, :instance, + dynamic_configuration: false +``` +4. Restart pleroma instance +```bash +sudo service pleroma restart +``` diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 590c7a914..bb126463c 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -9,27 +9,29 @@ defmodule Mix.Tasks.Pleroma.Config do alias Pleroma.Web.AdminAPI.Config @shortdoc "Manages the location of the config" @moduledoc File.read!("docs/administration/CLI_tasks/config.md") + + @groups [ + :pleroma, + :logger, + :quack, + :mime, + :tesla, + :phoenix, + :cors_plug, + :auto_linker, + :esshd, + :ueberauth, + :prometheus, + :http_signatures, + :web_push_encryption, + :joken + ] + def run(["migrate_to_db"]) do start_pleroma() if Pleroma.Config.get([:instance, :dynamic_configuration]) do - Application.get_all_env(:pleroma) - |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) - |> Enum.each(fn {k, v} -> - key = to_string(k) |> String.replace("Elixir.", "") - - key = - if String.starts_with?(key, "Pleroma.") do - key - else - ":" <> key - end - - {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v}) - Mix.shell().info("#{key} is migrated.") - end) - - Mix.shell().info("Settings migrated.") + Enum.each(@groups, &load_and_create(&1)) else Mix.shell().info( "Migration is not allowed by config. You can change this behavior in instance settings." @@ -37,38 +39,63 @@ def run(["migrate_to_db"]) do end end - def run(["migrate_from_db", env, delete?]) do + def run(["migrate_from_db" | options]) do start_pleroma() - delete? = if delete? == "true", do: true, else: false + {opts, _} = + OptionParser.parse!(options, + strict: [env: :string, delete_from_db: :boolean], + aliases: [d: :delete_from_db] + ) - if Pleroma.Config.get([:instance, :dynamic_configuration]) do - config_path = "config/#{env}.exported_from_db.secret.exs" - - {:ok, file} = File.open(config_path, [:write, :utf8]) + with {:active?, true} <- {:active?, Pleroma.Config.get([:instance, :dynamic_configuration])}, + env_path when is_binary(env_path) <- opts[:env], + config_path <- "config/#{env_path}.exported_from_db.secret.exs", + {:ok, file} <- File.open(config_path, [:write, :utf8]) do IO.write(file, "use Mix.Config\r\n") - Repo.all(Config) - |> Enum.each(fn config -> - IO.write( - file, - "config :#{config.group}, #{config.key}, #{ - inspect(Config.from_binary(config.value), limit: :infinity) - }\r\n\r\n" - ) - - if delete? do - {:ok, _} = Repo.delete(config) - Mix.shell().info("#{config.key} deleted from DB.") - end - end) + Config + |> Repo.all() + |> Enum.each(&write_to_file_with_deletion(&1, file, opts[:delete_from_db])) File.close(file) System.cmd("mix", ["format", config_path]) else - Mix.shell().info( - "Migration is not allowed by config. You can change this behavior in instance settings." - ) + {:active?, false} -> + Mix.shell().info( + "migration is not allowed by config. You can change this behavior in instance settings." + ) + + error -> + Mix.shell().info("error occuried while opening file. #{inspect(error)}") + end + end + + defp load_and_create(group) do + group + |> Application.get_all_env() + |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) + |> Enum.each(fn {key, value} -> + key_str = inspect(key) + + {:ok, _} = Config.update_or_create(%{group: ":#{group}", key: key_str, value: value}) + Mix.shell().info("settings for key #{key_str} migrated.") + end) + + Mix.shell().info("settings for group :#{group} migrated.") + end + + defp write_to_file_with_deletion(config, file, with_deletion) do + IO.write( + file, + "config #{config.group}, #{config.key}, #{ + inspect(Config.from_binary(config.value), limit: :infinity) + }\r\n\r\n" + ) + + if with_deletion do + {:ok, _} = Repo.delete(config) + Mix.shell().info("#{config.key} deleted from DB.") end end end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 3214c9951..0bc4c4029 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -4,56 +4,59 @@ defmodule Pleroma.Config.TransferTask do use Task + + require Logger + + alias Pleroma.Repo alias Pleroma.Web.AdminAPI.Config def start_link(_) do load_and_update_env() - if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo) + if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) :ignore end def load_and_update_env do - if Pleroma.Config.get([:instance, :dynamic_configuration]) and - Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do - for_restart = - Pleroma.Repo.all(Config) - |> Enum.map(&update_env(&1)) - + with true <- Pleroma.Config.get([:instance, :dynamic_configuration]), + true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"), + started_applications <- Application.started_applications() do # We need to restart applications for loaded settings take effect - for_restart - |> Enum.reject(&(&1 in [:pleroma, :ok])) - |> Enum.each(fn app -> - Application.stop(app) - :ok = Application.start(app) - end) + Config + |> Repo.all() + |> Enum.map(&update_env(&1)) + |> Enum.uniq() + # TODO: some problem with prometheus after restart! + |> Enum.reject(&(&1 in [:pleroma, nil, :prometheus])) + |> Enum.each(&restart(started_applications, &1)) end end defp update_env(setting) do try do - key = - if String.starts_with?(setting.key, "Pleroma.") do - "Elixir." <> setting.key - else - String.trim_leading(setting.key, ":") - end + key = Config.from_string(setting.key) + group = Config.from_string(setting.group) + value = Config.from_binary(setting.value) - group = String.to_existing_atom(setting.group) - - Application.put_env( - group, - String.to_existing_atom(key), - Config.from_binary(setting.value) - ) + :ok = Application.put_env(group, key, value) group rescue e -> - require Logger - Logger.warn( "updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}" ) + + nil + end + end + + defp restart(started_applications, app) do + with {^app, _, _} <- List.keyfind(started_applications, app, 0), + :ok <- Application.stop(app) do + :ok = Application.start(app) + else + nil -> Logger.warn("#{app} is not started.") + error -> Logger.warn(inspect(error)) end end end diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index aa578eee2..b57e47e8b 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -6,68 +6,108 @@ def process(implementation, descriptions) do implementation.process(descriptions) end - @spec uploaders_list() :: [module()] - def uploaders_list do - {:ok, modules} = :application.get_key(:pleroma, :modules) - - Enum.filter(modules, fn module -> - name_as_list = Module.split(module) - - List.starts_with?(name_as_list, ["Pleroma", "Uploaders"]) and - List.last(name_as_list) != "Uploader" - end) + @spec list_modules_in_dir(String.t(), String.t()) :: [module()] + def list_modules_in_dir(dir, start) do + with {:ok, files} <- File.ls(dir) do + files + |> Enum.filter(&String.ends_with?(&1, ".ex")) + |> Enum.map(fn filename -> + module = filename |> String.trim_trailing(".ex") |> Macro.camelize() + String.to_existing_atom(start <> module) + end) + end end - @spec filters_list() :: [module()] - def filters_list do - {:ok, modules} = :application.get_key(:pleroma, :modules) - - Enum.filter(modules, fn module -> - name_as_list = Module.split(module) - - List.starts_with?(name_as_list, ["Pleroma", "Upload", "Filter"]) - end) + @doc """ + Converts: + - atoms to strings with leading `:` + - module names to strings, without leading `Elixir.` + - add humanized labels to `keys` if label is not defined, e.g. `:instance` -> `Instance` + """ + @spec convert_to_strings([map()]) :: [map()] + def convert_to_strings(descriptions) do + Enum.map(descriptions, &format_entity(&1)) end - @spec mrf_list() :: [module()] - def mrf_list do - {:ok, modules} = :application.get_key(:pleroma, :modules) - - Enum.filter(modules, fn module -> - name_as_list = Module.split(module) - - List.starts_with?(name_as_list, ["Pleroma", "Web", "ActivityPub", "MRF"]) and - length(name_as_list) > 4 - end) + defp format_entity(entity) do + entity + |> format_key() + |> Map.put(:group, atom_to_string(entity[:group])) + |> format_children() end - @spec richmedia_parsers() :: [module()] - def richmedia_parsers do - {:ok, modules} = :application.get_key(:pleroma, :modules) - - Enum.filter(modules, fn module -> - name_as_list = Module.split(module) - - List.starts_with?(name_as_list, ["Pleroma", "Web", "RichMedia", "Parsers"]) and - length(name_as_list) == 5 - end) + defp format_key(%{key: key} = entity) do + entity + |> Map.put(:key, atom_to_string(key)) + |> Map.put(:label, entity[:label] || humanize(key)) end + + defp format_key(%{group: group} = entity) do + Map.put(entity, :label, entity[:label] || humanize(group)) + end + + defp format_key(entity), do: entity + + defp format_children(%{children: children} = entity) do + Map.put(entity, :children, Enum.map(children, &format_child(&1))) + end + + defp format_children(entity), do: entity + + defp format_child(%{suggestions: suggestions} = entity) do + entity + |> Map.put(:suggestions, format_suggestions(suggestions)) + |> format_key() + |> format_children() + end + + defp format_child(entity) do + entity + |> format_key() + |> format_children() + end + + defp atom_to_string(entity) when is_binary(entity), do: entity + + defp atom_to_string(entity) when is_atom(entity), do: inspect(entity) + + defp humanize(entity) do + string = inspect(entity) + + if String.starts_with?(string, ":"), + do: Phoenix.Naming.humanize(entity), + else: string + end + + defp format_suggestions([]), do: [] + + defp format_suggestions([suggestion | tail]) do + [format_suggestion(suggestion) | format_suggestions(tail)] + end + + defp format_suggestion(entity) when is_atom(entity) do + atom_to_string(entity) + end + + defp format_suggestion([head | tail] = entity) when is_list(entity) do + [format_suggestion(head) | format_suggestions(tail)] + end + + defp format_suggestion(entity) when is_tuple(entity) do + format_suggestions(Tuple.to_list(entity)) |> List.to_tuple() + end + + defp format_suggestion(entity), do: entity end defimpl Jason.Encoder, for: Tuple do - def encode(tuple, opts) do - Jason.Encode.list(Tuple.to_list(tuple), opts) - end + def encode(tuple, opts), do: Jason.Encode.list(Tuple.to_list(tuple), opts) end defimpl Jason.Encoder, for: [Regex, Function] do - def encode(term, opts) do - Jason.Encode.string(inspect(term), opts) - end + def encode(term, opts), do: Jason.Encode.string(inspect(term), opts) end defimpl String.Chars, for: Regex do - def to_string(term) do - inspect(term) - end + def to_string(term), do: inspect(term) end diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index f2a56d845..f191b6013 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -3,18 +3,22 @@ defmodule Pleroma.Docs.JSON do @spec process(keyword()) :: {:ok, String.t()} def process(descriptions) do - config_path = "docs/generate_config.json" - - with {:ok, file} <- File.open(config_path, [:write, :utf8]), - json <- generate_json(descriptions), + with path <- "docs/generated_config.json", + {:ok, file} <- File.open(path, [:write, :utf8]), + formatted_descriptions <- + Pleroma.Docs.Generator.convert_to_strings(descriptions), + json <- Jason.encode!(formatted_descriptions), :ok <- IO.write(file, json), :ok <- File.close(file) do - {:ok, config_path} + {:ok, path} end end - @spec generate_json([keyword()]) :: String.t() - def generate_json(descriptions) do - Jason.encode!(descriptions) + def compile do + with {config, _paths} <- Mix.Config.eval!("config/description.exs") do + config[:pleroma][:config_description] + |> Pleroma.Docs.Generator.convert_to_strings() + |> Jason.encode!() + end end end diff --git a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex similarity index 100% rename from lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex rename to lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex diff --git a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex similarity index 100% rename from lib/pleroma/web/activity_pub/mrf/noop_policy.ex rename to lib/pleroma/web/activity_pub/mrf/no_op_policy.ex diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex similarity index 100% rename from lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex rename to lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex index 4eaea00d8..9a03d67c0 100644 --- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex @@ -19,8 +19,7 @@ def filter(%{"type" => "Undo", "object" => child_message} = message) do def filter(%{"type" => message_type} = message) do with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]), rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]), - true <- - length(accepted_vocabulary) == 0 || Enum.member?(accepted_vocabulary, message_type), + true <- accepted_vocabulary == [] || Enum.member?(accepted_vocabulary, message_type), false <- length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type), {:ok, _} <- filter(message["object"]) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index c8abeff06..376f88061 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -4,6 +4,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + alias Pleroma.Activity alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug @@ -25,10 +28,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.Router - import Pleroma.Web.ControllerHelper, only: [json_response: 3] - require Logger + @descriptions_json Pleroma.Docs.JSON.compile() + @users_page_size 50 + plug( OAuthScopesPlug, %{scopes: ["read:accounts"], admin: true} @@ -93,8 +97,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do when action in [:relay_follow, :relay_unfollow, :config_update] ) - @users_page_size 50 - action_fallback(:errors) def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do @@ -782,10 +784,22 @@ def migrate_to_db(conn, _params) do end def migrate_from_db(conn, _params) do - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"]) + Mix.Tasks.Pleroma.Config.run([ + "migrate_from_db", + "--env", + to_string(Pleroma.Config.get(:env)), + "-d" + ]) + json(conn, %{}) end + def config_descriptions(conn, _params) do + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp(200, @descriptions_json) + end + def config_show(conn, _params) do configs = Pleroma.Repo.all(Config) @@ -800,17 +814,27 @@ def config_update(conn, %{"configs" => configs}) do updated = Enum.map(configs, fn %{"group" => group, "key" => key, "delete" => "true"} = params -> - {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]}) - config + with {:ok, config} <- + Config.delete(%{group: group, key: key, subkeys: params["subkeys"]}) do + config + end %{"group" => group, "key" => key, "value" => value} -> - {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) - config + with {:ok, config} <- + Config.update_or_create(%{group: group, key: key, value: value}) do + config + end end) |> Enum.reject(&is_nil(&1)) Pleroma.Config.TransferTask.load_and_update_env() - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"]) + + Mix.Tasks.Pleroma.Config.run([ + "migrate_from_db", + "--env", + to_string(Pleroma.Config.get(:env)) + ]) + updated else [] diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex index 1917a5580..a74acfbc6 100644 --- a/lib/pleroma/web/admin_api/config.ex +++ b/lib/pleroma/web/admin_api/config.ex @@ -24,6 +24,8 @@ def get_by_params(params), do: Repo.get_by(Config, params) @spec changeset(Config.t(), map()) :: Changeset.t() def changeset(config, params \\ %{}) do + params = Map.put(params, :value, transform(params[:value])) + config |> cast(params, [:key, :group, :value]) |> validate_required([:key, :group, :value]) @@ -33,42 +35,43 @@ def changeset(config, params \\ %{}) do @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} def create(params) do %Config{} - |> changeset(Map.put(params, :value, transform(params[:value]))) + |> changeset(params) |> Repo.insert() end @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()} def update(%Config{} = config, %{value: value}) do config - |> change(value: transform(value)) + |> changeset(%{value: value}) |> Repo.update() end @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} def update_or_create(params) do - with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do + search_opts = Map.take(params, [:group, :key]) + + with %Config{} = config <- Config.get_by_params(search_opts) do Config.update(config, params) else nil -> Config.create(params) end end - @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} | {:ok, nil} def delete(params) do - with %Config{} = config <- Config.get_by_params(Map.delete(params, :subkeys)) do - if params[:subkeys] do - updated_value = - Keyword.drop( - :erlang.binary_to_term(config.value), - Enum.map(params[:subkeys], &do_transform_string(&1)) - ) + search_opts = Map.delete(params, :subkeys) - Config.update(config, %{value: updated_value}) - else + with %Config{} = config <- Config.get_by_params(search_opts), + {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]}, + old_value <- :erlang.binary_to_term(config.value), + keys <- Enum.map(sub_keys, &do_transform_string(&1)), + new_value <- Keyword.drop(old_value, keys) do + Config.update(config, %{value: new_value}) + else + {config, nil} -> Repo.delete(config) {:ok, nil} - end - else + nil -> err = dgettext("errors", "Config with params %{params} not found", params: inspect(params)) @@ -82,10 +85,22 @@ def from_binary(binary), do: :erlang.binary_to_term(binary) @spec from_binary_with_convert(binary()) :: any() def from_binary_with_convert(binary) do - from_binary(binary) + binary + |> from_binary() |> do_convert() end + @spec from_string(String.t()) :: atom() | no_return() + def from_string(":" <> entity), do: String.to_existing_atom(entity) + + def from_string(entity) when is_binary(entity) do + if is_module_name?(entity) do + String.to_existing_atom("Elixir.#{entity}") + else + entity + end + end + defp do_convert(entity) when is_list(entity) do for v <- entity, into: [], do: do_convert(v) end @@ -97,6 +112,7 @@ defp do_convert(entity) when is_map(entity) do end defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]} + # TODO: will become useless after removing hackney defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]} defp do_convert(entity) when is_tuple(entity), @@ -105,21 +121,15 @@ defp do_convert(entity) when is_tuple(entity), defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity), do: entity - defp do_convert(entity) when is_atom(entity) do - string = to_string(entity) - - if String.starts_with?(string, "Elixir."), - do: do_convert(string), - else: ":" <> string - end - - defp do_convert("Elixir." <> module_name), do: module_name + defp do_convert(entity) when is_atom(entity), do: inspect(entity) defp do_convert(entity) when is_binary(entity), do: entity - @spec transform(any()) :: binary() + @spec transform(any()) :: binary() | no_return() def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do - :erlang.term_to_binary(do_transform(entity)) + entity + |> do_transform() + |> :erlang.term_to_binary() end def transform(entity), do: :erlang.term_to_binary(entity) @@ -131,6 +141,7 @@ defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do {:dispatch, [dispatch_settings]} end + # TODO: will become useless after removing hackney defp do_transform(%{"tuple" => [":partial_chain", entity]}) do {partial_chain, []} = do_eval(entity) {:partial_chain, partial_chain} @@ -149,34 +160,63 @@ defp do_transform(entity) when is_list(entity) do end defp do_transform(entity) when is_binary(entity) do - String.trim(entity) + entity + |> String.trim() |> do_transform_string() end defp do_transform(entity), do: entity - defp do_transform_string("~r/" <> pattern) do - modificator = String.split(pattern, "/") |> List.last() - pattern = String.trim_trailing(pattern, "/" <> modificator) + @delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}] - case modificator do - "" -> ~r/#{pattern}/ - "i" -> ~r/#{pattern}/i - "u" -> ~r/#{pattern}/u - "s" -> ~r/#{pattern}/s + defp find_valid_delimiter([], _string, _), + do: raise(ArgumentError, message: "valid delimiter for Regex expression not found") + + defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter) + when is_tuple(delimiter) do + if String.contains?(pattern, closing) do + find_valid_delimiter(others, pattern, regex_delimiter) + else + {:ok, {leading, closing}} + end + end + + defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do + if String.contains?(pattern, delimiter) do + find_valid_delimiter(others, pattern, regex_delimiter) + else + {:ok, {delimiter, delimiter}} + end + end + + @regex_parts ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u + + defp do_transform_string("~r" <> _pattern = regex) do + with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <- + Regex.named_captures(@regex_parts, regex), + {:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter), + {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do + result end end defp do_transform_string(":" <> atom), do: String.to_atom(atom) defp do_transform_string(value) do - if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"), - do: String.to_existing_atom("Elixir." <> value), - else: value + if is_module_name?(value) do + String.to_existing_atom("Elixir." <> value) + else + value + end + end + + @spec is_module_name?(String.t()) :: boolean() + def is_module_name?(string) do + Regex.match?(~r/^(Pleroma|Phoenix|Tesla)\./, string) or string in ["Oban", "Ueberauth"] end defp do_eval(entity) do cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") - Code.eval_string(cleaned_string, [], requires: [], macros: []) + Code.eval_string(cleaned_string) end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index f6c128283..a182e90e7 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -195,6 +195,7 @@ defmodule Pleroma.Web.Router do get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) + get("/config/descriptions", AdminAPIController, :config_descriptions) get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 9074f3b97..4b3dd8bbd 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -14,14 +14,14 @@ test "transfer config values from db to env" do refute Application.get_env(:idna, :test_key) Pleroma.Web.AdminAPI.Config.create(%{ - group: "pleroma", - key: "test_key", + group: ":pleroma", + key: ":test_key", value: [live: 2, com: 3] }) Pleroma.Web.AdminAPI.Config.create(%{ - group: "idna", - key: "test_key", + group: ":idna", + key: ":test_key", value: [live: 15, com: 35] }) @@ -38,14 +38,14 @@ test "transfer config values from db to env" do test "non existing atom" do Pleroma.Web.AdminAPI.Config.create(%{ - group: "pleroma", - key: "undefined_atom_key", + group: ":pleroma", + key: ":undefined_atom_key", value: [live: 2, com: 3] }) assert ExUnit.CaptureLog.capture_log(fn -> Pleroma.Config.TransferTask.start_link([]) end) =~ - "updating env causes error, key: \"undefined_atom_key\", error: %ArgumentError{message: \"argument error\"}" + "updating env causes error, key: \":undefined_atom_key\", error: %ArgumentError{message: \"argument error\"}" end end diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs new file mode 100644 index 000000000..42e7c32c8 --- /dev/null +++ b/test/docs/generator_test.exs @@ -0,0 +1,211 @@ +defmodule Pleroma.Docs.GeneratorTest do + use ExUnit.Case, async: true + alias Pleroma.Docs.Generator + + @descriptions [ + %{ + group: :pleroma, + key: Pleroma.Upload, + type: :group, + description: "", + children: [ + %{ + key: :uploader, + type: :module, + description: "", + suggestions: + Generator.list_modules_in_dir( + "lib/pleroma/upload/filter", + "Elixir.Pleroma.Upload.Filter." + ) + }, + %{ + key: :filters, + type: {:list, :module}, + description: "", + suggestions: + Generator.list_modules_in_dir( + "lib/pleroma/web/activity_pub/mrf", + "Elixir.Pleroma.Web.ActivityPub.MRF." + ) + }, + %{ + key: Pleroma.Upload, + type: :string, + description: "", + suggestions: [""] + }, + %{ + key: :some_key, + type: :keyword, + description: "", + suggestions: [], + children: [ + %{ + key: :another_key, + type: :integer, + description: "", + suggestions: [5] + }, + %{ + key: :another_key_with_label, + label: "Another label", + type: :integer, + description: "", + suggestions: [7] + } + ] + }, + %{ + key: :key1, + type: :atom, + description: "", + suggestions: [ + :atom, + Pleroma.Upload, + {:tuple, "string", 8080}, + [:atom, Pleroma.Upload, {:atom, Pleroma.Upload}] + ] + }, + %{ + key: Pleroma.Upload, + label: "Special Label", + type: :string, + description: "", + suggestions: [""] + }, + %{ + group: {:subgroup, Swoosh.Adapters.SMTP}, + key: :auth, + type: :atom, + description: "`Swoosh.Adapters.SMTP` adapter specific setting", + suggestions: [:always, :never, :if_available] + }, + %{ + key: "application/xml", + type: {:list, :string}, + suggestions: ["xml"] + } + ] + }, + %{ + group: :tesla, + key: :adapter, + type: :group, + description: "" + }, + %{ + group: :cors_plug, + type: :group, + children: [%{key: :key1, type: :string, suggestions: [""]}] + }, + %{group: "Some string group", key: "Some string key", type: :group} + ] + + describe "convert_to_strings/1" do + test "group, key, label" do + [desc1, desc2 | _] = Generator.convert_to_strings(@descriptions) + + assert desc1[:group] == ":pleroma" + assert desc1[:key] == "Pleroma.Upload" + assert desc1[:label] == "Pleroma.Upload" + + assert desc2[:group] == ":tesla" + assert desc2[:key] == ":adapter" + assert desc2[:label] == "Adapter" + end + + test "group without key" do + descriptions = Generator.convert_to_strings(@descriptions) + desc = Enum.at(descriptions, 2) + + assert desc[:group] == ":cors_plug" + refute desc[:key] + assert desc[:label] == "Cors plug" + end + + test "children key, label, type" do + [%{children: [child1, child2, child3, child4 | _]} | _] = + Generator.convert_to_strings(@descriptions) + + assert child1[:key] == ":uploader" + assert child1[:label] == "Uploader" + assert child1[:type] == :module + + assert child2[:key] == ":filters" + assert child2[:label] == "Filters" + assert child2[:type] == {:list, :module} + + assert child3[:key] == "Pleroma.Upload" + assert child3[:label] == "Pleroma.Upload" + assert child3[:type] == :string + + assert child4[:key] == ":some_key" + assert child4[:label] == "Some key" + assert child4[:type] == :keyword + end + + test "child with predefined label" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 5) + assert child[:key] == "Pleroma.Upload" + assert child[:label] == "Special Label" + end + + test "subchild" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 3) + %{children: [subchild | _]} = child + + assert subchild[:key] == ":another_key" + assert subchild[:label] == "Another key" + assert subchild[:type] == :integer + end + + test "subchild with predefined label" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 3) + %{children: subchildren} = child + subchild = Enum.at(subchildren, 1) + + assert subchild[:key] == ":another_key_with_label" + assert subchild[:label] == "Another label" + end + + test "module suggestions" do + [%{children: [%{suggestions: suggestions} | _]} | _] = + Generator.convert_to_strings(@descriptions) + + Enum.each(suggestions, fn suggestion -> + assert String.starts_with?(suggestion, "Pleroma.") + end) + end + + test "atoms in suggestions with leading `:`" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + %{suggestions: suggestions} = Enum.at(children, 4) + assert Enum.at(suggestions, 0) == ":atom" + assert Enum.at(suggestions, 1) == "Pleroma.Upload" + assert Enum.at(suggestions, 2) == {":tuple", "string", 8080} + assert Enum.at(suggestions, 3) == [":atom", "Pleroma.Upload", {":atom", "Pleroma.Upload"}] + + %{suggestions: suggestions} = Enum.at(children, 6) + assert Enum.at(suggestions, 0) == ":always" + assert Enum.at(suggestions, 1) == ":never" + assert Enum.at(suggestions, 2) == ":if_available" + end + + test "group, key as string in main desc" do + descriptions = Generator.convert_to_strings(@descriptions) + desc = Enum.at(descriptions, 3) + assert desc[:group] == "Some string group" + assert desc[:key] == "Some string key" + end + + test "key as string subchild" do + [%{children: children} | _] = Generator.convert_to_strings(@descriptions) + child = Enum.at(children, 7) + assert child[:key] == "application/xml" + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 314f26ec9..a7aa54f73 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -377,8 +377,8 @@ def registration_factory do def config_factory do %Pleroma.Web.AdminAPI.Config{ - key: sequence(:key, &"some_key_#{&1}"), - group: "pleroma", + key: sequence(:key, &":some_key_#{&1}"), + group: ":pleroma", value: sequence( :value, diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index fab9d6e9a..055f678b9 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -9,16 +9,14 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do setup_all do Mix.shell(Mix.Shell.Process) - temp_file = "config/temp.exported_from_db.secret.exs" on_exit(fn -> Mix.shell(Mix.Shell.IO) Application.delete_env(:pleroma, :first_setting) Application.delete_env(:pleroma, :second_setting) - :ok = File.rm(temp_file) end) - {:ok, temp_file: temp_file} + :ok end clear_config_all([:instance, :dynamic_configuration]) do @@ -28,38 +26,44 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do test "settings are migrated to db" do assert Repo.all(Config) == [] - Application.put_env(:pleroma, :first_setting, key: "value", key2: [Pleroma.Repo]) - Application.put_env(:pleroma, :second_setting, key: "value2", key2: [Pleroma.Activity]) + Application.put_env(:pleroma, :first_setting, key: "value", key2: [Repo]) + Application.put_env(:pleroma, :second_setting, key: "value2", key2: ["Activity"]) Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) - first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"}) - second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"}) - refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"}) + config1 = Config.get_by_params(%{group: ":pleroma", key: ":first_setting"}) + config2 = Config.get_by_params(%{group: ":pleroma", key: ":second_setting"}) + refute Config.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) - assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]] - assert Config.from_binary(second_db.value) == [key: "value2", key2: [Pleroma.Activity]] + assert Config.from_binary(config1.value) == [key: "value", key2: [Repo]] + assert Config.from_binary(config2.value) == [key: "value2", key2: ["Activity"]] end - test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do + test "settings are migrated to file and deleted from db" do + env = "temp" + config_file = "config/#{env}.exported_from_db.secret.exs" + + on_exit(fn -> + :ok = File.rm(config_file) + end) + Config.create(%{ - group: "pleroma", + group: ":pleroma", key: ":setting_first", - value: [key: "value", key2: [Pleroma.Activity]] + value: [key: "value", key2: ["Activity"]] }) Config.create(%{ - group: "pleroma", + group: ":pleroma", key: ":setting_second", - value: [key: "valu2", key2: [Pleroma.Repo]] + value: [key: "value2", key2: [Repo]] }) - Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"]) + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "--env", env, "-d"]) assert Repo.all(Config) == [] - assert File.exists?(temp_file) - {:ok, file} = File.read(temp_file) + file = File.read!(config_file) assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_second," end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 49ff005b6..fd54504ac 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1950,6 +1950,7 @@ test "with settings in db", %{conn: conn} do %{ "configs" => [ %{ + "group" => ":pleroma", "key" => key1, "value" => _ }, @@ -1995,15 +1996,15 @@ test "create new config setting in db", %{conn: conn} do conn = post(conn, "/api/pleroma/admin/config", %{ configs: [ - %{group: "pleroma", key: "key1", value: "value1"}, + %{group: ":pleroma", key: ":key1", value: "value1"}, %{ - group: "ueberauth", + group: ":ueberauth", key: "Ueberauth.Strategy.Twitter.OAuth", value: [%{"tuple" => [":consumer_secret", "aaaa"]}] }, %{ - group: "pleroma", - key: "key2", + group: ":pleroma", + key: ":key2", value: %{ ":nested_1" => "nested_value1", ":nested_2" => [ @@ -2013,21 +2014,21 @@ test "create new config setting in db", %{conn: conn} do } }, %{ - group: "pleroma", - key: "key3", + group: ":pleroma", + key: ":key3", value: [ %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, %{"nested_4" => true} ] }, %{ - group: "pleroma", - key: "key4", + group: ":pleroma", + key: ":key4", value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} }, %{ - group: "idna", - key: "key5", + group: ":idna", + key: ":key5", value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} } ] @@ -2036,18 +2037,18 @@ test "create new config setting in db", %{conn: conn} do assert json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "pleroma", - "key" => "key1", + "group" => ":pleroma", + "key" => ":key1", "value" => "value1" }, %{ - "group" => "ueberauth", + "group" => ":ueberauth", "key" => "Ueberauth.Strategy.Twitter.OAuth", "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}] }, %{ - "group" => "pleroma", - "key" => "key2", + "group" => ":pleroma", + "key" => ":key2", "value" => %{ ":nested_1" => "nested_value1", ":nested_2" => [ @@ -2057,21 +2058,21 @@ test "create new config setting in db", %{conn: conn} do } }, %{ - "group" => "pleroma", - "key" => "key3", + "group" => ":pleroma", + "key" => ":key3", "value" => [ %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, %{"nested_4" => true} ] }, %{ - "group" => "pleroma", - "key" => "key4", + "group" => ":pleroma", + "key" => ":key4", "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"} }, %{ - "group" => "idna", - "key" => "key5", + "group" => ":idna", + "key" => ":key5", "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} } ] @@ -2101,8 +2102,8 @@ test "create new config setting in db", %{conn: conn} do end test "update config setting & delete", %{conn: conn} do - config1 = insert(:config, key: "keyaa1") - config2 = insert(:config, key: "keyaa2") + config1 = insert(:config, key: ":keyaa1") + config2 = insert(:config, key: ":keyaa2") insert(:config, group: "ueberauth", @@ -2126,7 +2127,7 @@ test "update config setting & delete", %{conn: conn} do assert json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => config1.key, "value" => "another_value" } @@ -2138,11 +2139,14 @@ test "update config setting & delete", %{conn: conn} do end test "common config example", %{conn: conn} do + adapter = Application.get_env(:tesla, :adapter) + on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end) + conn = post(conn, "/api/pleroma/admin/config", %{ configs: [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => "Pleroma.Captcha.NotReal", "value" => [ %{"tuple" => [":enabled", false]}, @@ -2154,16 +2158,21 @@ test "common config example", %{conn: conn} do %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]} + %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} ] - } + }, + %{"group" => ":tesla", "key" => ":adapter", "value" => "Tesla.Adapter.Httpc"} ] }) + assert Application.get_env(:tesla, :adapter) == Tesla.Adapter.Httpc + assert Pleroma.Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" + assert json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => "Pleroma.Captcha.NotReal", "value" => [ %{"tuple" => [":enabled", false]}, @@ -2175,9 +2184,11 @@ test "common config example", %{conn: conn} do %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]} + %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} ] - } + }, + %{"group" => ":tesla", "key" => ":adapter", "value" => "Tesla.Adapter.Httpc"} ] } end @@ -2187,7 +2198,7 @@ test "tuples with more than two values", %{conn: conn} do post(conn, "/api/pleroma/admin/config", %{ configs: [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => "Pleroma.Web.Endpoint.NotReal", "value" => [ %{ @@ -2251,7 +2262,7 @@ test "tuples with more than two values", %{conn: conn} do assert json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => "Pleroma.Web.Endpoint.NotReal", "value" => [ %{ @@ -2318,7 +2329,7 @@ test "settings with nesting map", %{conn: conn} do post(conn, "/api/pleroma/admin/config", %{ configs: [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => ":key1", "value" => [ %{"tuple" => [":key2", "some_val"]}, @@ -2348,7 +2359,7 @@ test "settings with nesting map", %{conn: conn} do %{ "configs" => [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => ":key1", "value" => [ %{"tuple" => [":key2", "some_val"]}, @@ -2380,7 +2391,7 @@ test "value as map", %{conn: conn} do post(conn, "/api/pleroma/admin/config", %{ configs: [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => ":key1", "value" => %{"key" => "some_val"} } @@ -2391,7 +2402,7 @@ test "value as map", %{conn: conn} do %{ "configs" => [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => ":key1", "value" => %{"key" => "some_val"} } @@ -2404,7 +2415,7 @@ test "dispatch setting", %{conn: conn} do post(conn, "/api/pleroma/admin/config", %{ configs: [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => "Pleroma.Web.Endpoint.NotReal", "value" => [ %{ @@ -2437,7 +2448,7 @@ test "dispatch setting", %{conn: conn} do assert json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "pleroma", + "group" => ":pleroma", "key" => "Pleroma.Web.Endpoint.NotReal", "value" => [ %{ @@ -2467,7 +2478,7 @@ test "queues key as atom", %{conn: conn} do post(conn, "/api/pleroma/admin/config", %{ configs: [ %{ - "group" => "oban", + "group" => ":oban", "key" => ":queues", "value" => [ %{"tuple" => [":federator_incoming", 50]}, @@ -2485,7 +2496,7 @@ test "queues key as atom", %{conn: conn} do assert json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "oban", + "group" => ":oban", "key" => ":queues", "value" => [ %{"tuple" => [":federator_incoming", 50]}, @@ -2504,7 +2515,7 @@ test "queues key as atom", %{conn: conn} do test "delete part of settings by atom subkeys", %{conn: conn} do config = insert(:config, - key: "keyaa1", + key: ":keyaa1", value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") ) @@ -2524,8 +2535,8 @@ test "delete part of settings by atom subkeys", %{conn: conn} do json_response(conn, 200) == %{ "configs" => [ %{ - "group" => "pleroma", - "key" => "keyaa1", + "group" => ":pleroma", + "key" => ":keyaa1", "value" => [%{"tuple" => [":subkey2", "val2"]}] } ] @@ -3099,6 +3110,21 @@ test "it deletes the note", %{admin: admin, report_id: report_id} do assert ReportNote |> Repo.all() |> length() == 1 end end + + test "GET /api/pleroma/admin/config/descriptions", %{conn: conn} do + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + assert [child | _others] = json_response(conn, 200) + + assert child["children"] + assert child["key"] + assert String.starts_with?(child["group"], ":") + assert child["description"] + end end # Needed for testing diff --git a/test/web/admin_api/config_test.exs b/test/web/admin_api/config_test.exs index 204446b79..bff31bb85 100644 --- a/test/web/admin_api/config_test.exs +++ b/test/web/admin_api/config_test.exs @@ -91,14 +91,26 @@ test "pleroma module" do assert Config.from_binary(binary) == Pleroma.Bookmark end + test "pleroma string" do + binary = Config.transform("Pleroma") + assert binary == :erlang.term_to_binary("Pleroma") + assert Config.from_binary(binary) == "Pleroma" + end + test "phoenix module" do binary = Config.transform("Phoenix.Socket.V1.JSONSerializer") assert binary == :erlang.term_to_binary(Phoenix.Socket.V1.JSONSerializer) assert Config.from_binary(binary) == Phoenix.Socket.V1.JSONSerializer end + test "tesla module" do + binary = Config.transform("Tesla.Adapter.Hackney") + assert binary == :erlang.term_to_binary(Tesla.Adapter.Hackney) + assert Config.from_binary(binary) == Tesla.Adapter.Hackney + end + test "sigil" do - binary = Config.transform("~r/comp[lL][aA][iI][nN]er/") + binary = Config.transform("~r[comp[lL][aA][iI][nN]er]") assert binary == :erlang.term_to_binary(~r/comp[lL][aA][iI][nN]er/) assert Config.from_binary(binary) == ~r/comp[lL][aA][iI][nN]er/ end @@ -109,10 +121,10 @@ test "link sigil" do assert Config.from_binary(binary) == ~r/https:\/\/example.com/ end - test "link sigil with u modifier" do - binary = Config.transform("~r/https:\/\/example.com/u") - assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/u) - assert Config.from_binary(binary) == ~r/https:\/\/example.com/u + test "link sigil with um modifiers" do + binary = Config.transform("~r/https:\/\/example.com/um") + assert binary == :erlang.term_to_binary(~r/https:\/\/example.com/um) + assert Config.from_binary(binary) == ~r/https:\/\/example.com/um end test "link sigil with i modifier" do @@ -127,6 +139,12 @@ test "link sigil with s modifier" do assert Config.from_binary(binary) == ~r/https:\/\/example.com/s end + test "raise if valid delimiter not found" do + assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn -> + Config.transform("~r/https://[]{}<>\"'()|example.com/s") + end + end + test "2 child tuple" do binary = Config.transform(%{"tuple" => ["v1", ":v2"]}) assert binary == :erlang.term_to_binary({"v1", :v2})