Merge branch 'reactions' into 'develop'
authorrinpatch <rinpatch@sdf.org>
Thu, 14 Nov 2019 08:47:10 +0000 (08:47 +0000)
committerrinpatch <rinpatch@sdf.org>
Thu, 14 Nov 2019 08:47:10 +0000 (08:47 +0000)
Emoji Reactions

See merge request pleroma/pleroma!1662

43 files changed:
.gitlab-ci.yml
CHANGELOG.md
config/config.exs
docs/configuration/cheatsheet.md
docs/installation/openbsd_en.md
docs/installation/otp_en.md
lib/mix/tasks/pleroma/config.ex
lib/pleroma/application.ex
lib/pleroma/docs/json.ex
lib/pleroma/object/containment.ex
lib/pleroma/plugs/rate_limiter.ex [deleted file]
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex [new file with mode: 0644]
lib/pleroma/plugs/rate_limiter/rate_limiter.ex [new file with mode: 0644]
lib/pleroma/plugs/rate_limiter/supervisor.ex [new file with mode: 0644]
lib/pleroma/plugs/static_fe_plug.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mongooseim/mongoose_im_controller.ex
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/ostatus/ostatus_controller.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/static_fe/static_fe_controller.ex [new file with mode: 0644]
lib/pleroma/web/static_fe/static_fe_view.ex [new file with mode: 0644]
lib/pleroma/web/templates/layout/static_fe.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/static_fe/static_fe/error.html.eex [new file with mode: 0644]
lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex [new file with mode: 0644]
mix.exs
mix.lock
priv/static/static/static-fe.css [new file with mode: 0644]
test/object/containment_test.exs
test/plugs/rate_limiter_test.exs
test/web/admin_api/admin_api_controller_test.exs
test/web/node_info_test.exs
test/web/oauth/oauth_controller_test.exs
test/web/static_fe/static_fe_controller_test.exs [new file with mode: 0644]

index 0f8a0659b7ae9c10aca62eb52d44fb990fd646ce..d915ebae93aee674b13325d9657fbb6b8102fb39 100644 (file)
@@ -34,7 +34,7 @@ benchmark:
   variables:
     MIX_ENV: benchmark
   services:
-  - name: lainsoykaf/postgres-with-rum
+  - name: postgres:9.6
     alias: postgres
     command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
   script:
@@ -46,7 +46,7 @@ benchmark:
 unit-testing:
   stage: test
   services:
-  - name: lainsoykaf/postgres-with-rum
+  - name: postgres:9.6
     alias: postgres
     command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
   script:
@@ -58,7 +58,7 @@ unit-testing:
 unit-testing-rum:
   stage: test
   services:
-  - name: lainsoykaf/postgres-with-rum
+  - name: minibikini/postgres-with-rum:12
     alias: postgres
     command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
   variables:
@@ -113,6 +113,7 @@ review_app:
     - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
     - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
     - (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true
+    - (ssh -t dokku@pleroma.online -- git:set "$CI_ENVIRONMENT_SLUG" keep-git-dir true) || true
     - ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku
     - (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true
     - (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true
@@ -138,7 +139,7 @@ stop_review_app:
     - ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db
 
 amd64:
-  stage: release 
+  stage: release
   # TODO: Replace with upstream image when 1.9.0 comes out
   image: rinpatch/elixir:1.9.0-rc.0
   only: &release-only
index 0464f457124a1066a18838ea867f020e30c62f98..e04b96281661c8f328476dcb3b2a5217619bc4ee 100644 (file)
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Changed
 - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
+- **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default
 - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
 - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
 - Enabled `:instance, extended_nickname_format` in the default config
@@ -37,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Added
 - Refreshing poll results for remote polls
 - Authentication: Added rate limit for password-authorized actions / login existence checks
+- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
 - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
 - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
 <details>
@@ -65,6 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 - Report emails now include functional links to profiles of remote user accounts
+- Not being able to log in to some third-party apps when logged in to MastoFE
 <details>
   <summary>API Changes</summary>
 
index 7e1fe2a81f8856fc3ea163248c350c625bc5a5a4..75f463797643c895c10aa4672738e7c475dfc372 100644 (file)
@@ -90,7 +90,7 @@ config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.
 config :pleroma, Pleroma.Upload,
   uploader: Pleroma.Uploaders.Local,
   filters: [Pleroma.Upload.Filter.Dedupe],
-  link_name: true,
+  link_name: false,
   proxy_remote: false,
   proxy_opts: [
     redirect_on_failure: false,
@@ -257,7 +257,7 @@ config :pleroma, :instance,
   mrf_transparency_exclusions: [],
   autofollowed_nicknames: [],
   max_pinned_statuses: 1,
-  no_attachment_links: false,
+  no_attachment_links: true,
   welcome_user_nickname: nil,
   welcome_message: nil,
   max_report_comment_size: 1000,
@@ -274,7 +274,7 @@ config :pleroma, :instance,
   account_field_name_length: 512,
   account_field_value_length: 2048,
   external_user_synchronization: true,
-  extended_nickname_format: false
+  extended_nickname_format: true
 
 config :pleroma, :feed,
   post_title: %{
@@ -605,6 +605,8 @@ config :pleroma, Pleroma.ActivityExpiration, enabled: true
 
 config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
 
+config :pleroma, :static_fe, enabled: false
+
 config :pleroma, :web_cache_ttl,
   activity_pub: nil,
   activity_pub_question: 30_000
index 8f609fcfdbfe555afcdefaee21a474d884ae66e9..7832f69621a2a1522a14d4cf5ab5880ec4b77eb2 100644 (file)
@@ -180,6 +180,14 @@ config :pleroma, :frontend_configurations,
 
 These settings **need to be complete**, they will override the defaults.
 
+### :static_fe
+
+Render profiles and posts using server-generated HTML that is viewable without using JavaScript.
+
+Available options:
+
+* `enabled` - Enables the rendering of static HTML. Defaults to `false`.
+
 ### :assets
 
 This section configures assets to be used with various frontends. Currently the only option
@@ -523,7 +531,7 @@ config :pleroma, :workers,
 
 Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
 
-See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options. 
+See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
 
 Example:
 
@@ -593,6 +601,10 @@ See the [Quack Github](https://github.com/azohra/quack) for more details
 ## Database options
 
 ### RUM indexing for full text search
+
+!!! warning
+    It is recommended to use PostgreSQL v11 or newer. We have seen some minor issues with lower PostgreSQL versions.
+
 * `rum_enabled`: If RUM indexes should be used. Defaults to `false`.
 
 RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. While they may eventually be mainlined, for now they have to be installed as a PostgreSQL extension from https://github.com/postgrespro/rum.
@@ -793,4 +805,3 @@ config :auto_linker,
     rel: "ugc"
   ]
 ```
-
index 3585a326ba181935f67292f5c588d81f3d5aa6ad..45602bd757c4fd58f0c539e41ec978abe67c7a1f 100644 (file)
@@ -1,9 +1,13 @@
 # Installing on OpenBSD
+
 This guide describes the installation and configuration of pleroma (and the required software to run it) on a single OpenBSD 6.4 server.
+
 For any additional information regarding commands and configuration files mentioned here, check the man pages [online](https://man.openbsd.org/) or directly on your server with the man command.
 
 #### Required software
+
 The following packages need to be installed:
+
   * elixir
   * gmake
   * ImageMagick
@@ -11,8 +15,11 @@ The following packages need to be installed:
   * postgresql-server
   * postgresql-contrib
 
-To install them, run the following command (with doas or as root):  
-`pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib`
+To install them, run the following command (with doas or as root):
+
+```
+pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib
+```
 
 Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt.
 
@@ -31,8 +38,8 @@ Create the \_pleroma user, assign it the pleroma login class and create its home
 #### Clone pleroma's directory
 Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
 
-#### Postgresql
-Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:  
+#### PostgreSQL
+Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:
 If you wish to not use the default location for postgresql's data (/var/postgresql/data), add the following switch at the end of the command: `-D <path>` and modify the `datadir` variable in the /etc/rc.d/postgresql script.
 
 When this is done, enable postgresql so that it starts on boot and start it. As root, run:
@@ -44,6 +51,7 @@ To check that it started properly and didn't fail right after starting, you can
 
 #### httpd
 httpd will have three fuctions:
+
   * redirect requests trying to reach the instance over http to the https URL
   * serve a robots.txt file
   * get Let's Encrypt certificates, with acme-client
@@ -76,9 +84,9 @@ types {
        include "/usr/share/misc/mime.types"
 }
 ```
-Do not forget to change *\<IPv4/6 address\>* to your server's address(es). If httpd should only listen on one protocol family, comment one of the two first *listen* options.
+Do not forget to change *<IPv4/6 address\>* to your server's address(es). If httpd should only listen on one protocol family, comment one of the two first *listen* options.
 
-Create the /var/www/htdocs/local/ folder and write the content of your robots.txt in /var/www/htdocs/local/robots.txt.  
+Create the /var/www/htdocs/local/ folder and write the content of your robots.txt in /var/www/htdocs/local/robots.txt.
 Check the configuration with `httpd -n`, if it is OK enable and start httpd (as root):
 ```
 rcctl enable httpd
@@ -86,7 +94,7 @@ rcctl start httpd
 ```
 
 #### acme-client
-acme-client is used to get SSL/TLS certificates from Let's Encrypt. 
+acme-client is used to get SSL/TLS certificates from Let's Encrypt.
 Insert the following configuration in /etc/acme-client.conf:
 ```
 #
@@ -107,7 +115,7 @@ domain <domain name> {
        challengedir "/var/www/acme/"
 }
 ```
-Replace *\<domain name\>* by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv <domain name>` to create account and domain keys, and request a certificate for the first time.  
+Replace *<domain name\>* by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv <domain name>` to create account and domain keys, and request a certificate for the first time.
 Make acme-client run everyday by adding it in /etc/daily.local. As root, run the following command: `echo "acme-client <domain name>" >> /etc/daily.local`.
 
 Relayd will look for certificates and keys based on the address it listens on (see next part), the easiest way to make them available to relayd is to create a link, as root run:
@@ -118,7 +126,7 @@ ln -s /etc/ssl/private/<domain name>.key /etc/ssl/private/<IP address>.key
 This will have to be done for each IPv4 and IPv6 address relayd listens on.
 
 #### relayd
-relayd will be used as the reverse proxy sitting in front of pleroma. 
+relayd will be used as the reverse proxy sitting in front of pleroma.
 Insert the following configuration in /etc/relayd.conf:
 ```
 # $OpenBSD: relayd.conf,v 1.4 2018/03/23 09:55:06 claudio Exp $
@@ -169,7 +177,7 @@ relay wwwtls {
        forward to <httpd_server> port 80 check http "/robots.txt" code 200
 }
 ```
-Again, change *\<IPv4/6 address\>* to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://\<your instance's domain name\>*.  
+Again, change *<IPv4/6 address\>* to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://<your instance's domain name\>*.
 Check the configuration with `relayd -n`, if it is OK enable and start relayd (as root):
 ```
 rcctl enable relayd
@@ -177,7 +185,7 @@ rcctl start relayd
 ```
 
 #### pf
-Enabling and configuring pf is highly recommended.  
+Enabling and configuring pf is highly recommended.
 In /etc/pf.conf, insert the following configuration:
 ```
 # Macros
@@ -202,20 +210,22 @@ pass in quick on $if inet6 proto icmp6 to ($if) icmp6-type { echoreq unreach par
 pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd
 pass in quick on $if proto tcp from $authorized_ssh_clients to ($if) port ssh
 ```
-Replace *\<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for exemple, your home IP address, to avoid SSH connection attempts from bots.
+Replace *<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for exemple, your home IP address, to avoid SSH connection attempts from bots.
 
 Check pf's configuration by running `pfctl -nf /etc/pf.conf`, load it with `pfctl -f /etc/pf.conf` and enable pf at boot with `rcctl enable pf`.
 
 #### Configure and start pleroma
-Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's installation directory (`cd ~/pleroma/`).  
+Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's installation directory (`cd ~/pleroma/`).
+
 Then follow the main installation guide:
+
   * run `mix deps.get`
   * run `mix pleroma.instance gen` and enter your instance's information when asked
   * copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK.
   * exit your current shell back to a root one and run `psql -U postgres -f /home/_pleroma/config/setup_db.psql` to setup the database.
   * return to a \_pleroma shell into pleroma's installation directory (`su _pleroma -;cd ~/pleroma`) and run `MIX_ENV=prod mix ecto.migrate`
 
-As \_pleroma in /home/\_pleroma/pleroma, you can now run `LC_ALL=en_US.UTF-8 MIX_ENV=prod mix phx.server` to start your instance.  
+As \_pleroma in /home/\_pleroma/pleroma, you can now run `LC_ALL=en_US.UTF-8 MIX_ENV=prod mix phx.server` to start your instance.
 In another SSH session/tmux window, check that it is working properly by running `ftp -MVo - http://127.0.0.1:4000/api/v1/instance`, you should get json output. Double-check that *uri*'s value is your instance's domain name.
 
 ##### Starting pleroma at boot
index c028f4229983ff82c3e7c9e677732e9a16972925..965e30e2ac87073339551a47e63b78cb62a3dfa3 100644 (file)
@@ -42,6 +42,10 @@ apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot
 ## Setup
 ### Configuring PostgreSQL
 #### (Optional) Installing RUM indexes
+
+!!! warning
+    It is recommended to use PostgreSQL v11 or newer. We have seen some minor issues with lower PostgreSQL versions.
+
 RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. You can read more about them on the [Configuration page](../configuration/cheatsheet.md#rum-indexing-for-full-text-search). They are completely optional and most of the time are not worth it, especially if you are running a single user instance (unless you absolutely need ordered search results).
 
 Debian/Ubuntu (available only on Buster/19.04):
@@ -74,7 +78,7 @@ rc-service postgresql restart
 # Create the Pleroma user
 adduser --system --shell  /bin/false --home /opt/pleroma pleroma
 
-# Set the flavour environment variable to the string you got in Detecting flavour section. 
+# Set the flavour environment variable to the string you got in Detecting flavour section.
 # For example if the flavour is `arm64-musl` the command will be
 export FLAVOUR="arm64-musl"
 
@@ -180,7 +184,7 @@ rc-service pleroma start
 rc-update add pleroma
 ```
 
-If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors. 
+If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors.
 
 Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://webchat.freenode.net/?channels=%23pleroma) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma/issues/new)
 
index 11e4fde43eaa7932c817461a61a5b8231d6b87cb..0e21408b2b8b315cde8248fff16aeb9540c077b1 100644 (file)
@@ -45,7 +45,7 @@ defmodule Mix.Tasks.Pleroma.Config do
     if Pleroma.Config.get([:instance, :dynamic_configuration]) do
       config_path = "config/#{env}.exported_from_db.secret.exs"
 
-      {:ok, file} = File.open(config_path, [:write])
+      {:ok, file} = File.open(config_path, [:write, :utf8])
       IO.write(file, "use Mix.Config\r\n")
 
       Repo.all(Config)
index d681eecc80f95920577e6c81263800c6e3d89d53..2b6a55f98fda23c120db6e170374a633fa92b1c3 100644 (file)
@@ -36,7 +36,8 @@ defmodule Pleroma.Application do
         Pleroma.Emoji,
         Pleroma.Captcha,
         Pleroma.Daemons.ScheduledActivityDaemon,
-        Pleroma.Daemons.ActivityExpirationDaemon
+        Pleroma.Daemons.ActivityExpirationDaemon,
+        Pleroma.Plugs.RateLimiter.Supervisor
       ] ++
         cachex_children() ++
         hackney_pool_children() ++
index 18ba01d58aca56bb58b5edd38bdc738bb10d87c6..f2a56d845e594752db66cd24d67483be0f9f6221 100644 (file)
@@ -5,7 +5,7 @@ defmodule Pleroma.Docs.JSON do
   def process(descriptions) do
     config_path = "docs/generate_config.json"
 
-    with {:ok, file} <- File.open(config_path, [:write]),
+    with {:ok, file} <- File.open(config_path, [:write, :utf8]),
          json <- generate_json(descriptions),
          :ok <- IO.write(file, json),
          :ok <- File.close(file) do
index a1f9c1250b3248c41cc79bf39042bda7b0101143..25aa32f60743dc21fef01e1184539b3b70a77638 100644 (file)
@@ -64,6 +64,8 @@ defmodule Pleroma.Object.Containment do
   def contain_origin(id, %{"attributedTo" => actor} = params),
     do: contain_origin(id, Map.put(params, "actor", actor))
 
+  def contain_origin(_id, _data), do: :error
+
   def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
     id_uri = URI.parse(id)
     other_uri = URI.parse(other_id)
diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex
deleted file mode 100644 (file)
index 31388f5..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Plugs.RateLimiter do
-  @moduledoc """
-
-  ## Configuration
-
-  A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
-
-  * The first element: `scale` (Integer). The time scale in milliseconds.
-  * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
-
-  It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
-
-  To disable a limiter set its value to `nil`.
-
-  ### Example
-
-      config :pleroma, :rate_limit,
-        one: {1000, 10},
-        two: [{10_000, 10}, {10_000, 50}],
-        foobar: nil
-
-  Here we have three limiters:
-
-  * `one` which is not over 10req/1s
-  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
-  * `foobar` which is disabled
-
-  ## Usage
-
-  AllowedSyntax:
-
-      plug(Pleroma.Plugs.RateLimiter, :limiter_name)
-      plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
-
-  Allowed options:
-
-      * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
-      * `params` appends values of specified request params (e.g. ["id"]) to bucket name
-
-  Inside a controller:
-
-      plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
-      plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
-
-      plug(
-        Pleroma.Plugs.RateLimiter,
-        {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
-        when action in ~w(fav_status unfav_status)a
-      )
-
-  or inside a router pipeline:
-
-      pipeline :api do
-        ...
-        plug(Pleroma.Plugs.RateLimiter, :one)
-        ...
-      end
-  """
-  import Pleroma.Web.TranslationHelpers
-  import Plug.Conn
-
-  alias Pleroma.User
-
-  def init(limiter_name) when is_atom(limiter_name) do
-    init({limiter_name, []})
-  end
-
-  def init({limiter_name, opts}) do
-    case Pleroma.Config.get([:rate_limit, limiter_name]) do
-      nil -> nil
-      config -> {limiter_name, config, opts}
-    end
-  end
-
-  # Do not limit if there is no limiter configuration
-  def call(conn, nil), do: conn
-
-  def call(conn, settings) do
-    case check_rate(conn, settings) do
-      {:ok, _count} ->
-        conn
-
-      {:error, _count} ->
-        render_throttled_error(conn)
-    end
-  end
-
-  defp bucket_name(conn, limiter_name, opts) do
-    bucket_name = opts[:bucket_name] || limiter_name
-
-    if params_names = opts[:params] do
-      params_values = for p <- Enum.sort(params_names), do: conn.params[p]
-      Enum.join([bucket_name] ++ params_values, ":")
-    else
-      bucket_name
-    end
-  end
-
-  defp check_rate(
-         %{assigns: %{user: %User{id: user_id}}} = conn,
-         {limiter_name, [_, {scale, limit}], opts}
-       ) do
-    bucket_name = bucket_name(conn, limiter_name, opts)
-    ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
-  end
-
-  defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
-    bucket_name = bucket_name(conn, limiter_name, opts)
-    ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
-  end
-
-  defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
-    check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
-  end
-
-  def ip(%{remote_ip: remote_ip}) do
-    remote_ip
-    |> Tuple.to_list()
-    |> Enum.join(".")
-  end
-
-  defp render_throttled_error(conn) do
-    conn
-    |> render_error(:too_many_requests, "Throttled")
-    |> halt()
-  end
-end
diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
new file mode 100644 (file)
index 0000000..187582e
--- /dev/null
@@ -0,0 +1,44 @@
+defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
+  use DynamicSupervisor
+
+  import Cachex.Spec
+
+  def start_link(init_arg) do
+    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
+  end
+
+  def add_limiter(limiter_name, expiration) do
+    {:ok, _pid} =
+      DynamicSupervisor.start_child(
+        __MODULE__,
+        %{
+          id: String.to_atom("rl_#{limiter_name}"),
+          start:
+            {Cachex, :start_link,
+             [
+               limiter_name,
+               [
+                 expiration:
+                   expiration(
+                     default: expiration,
+                     interval: check_interval(expiration),
+                     lazy: true
+                   )
+               ]
+             ]}
+        }
+      )
+  end
+
+  @impl true
+  def init(_init_arg) do
+    DynamicSupervisor.init(strategy: :one_for_one)
+  end
+
+  defp check_interval(exp) do
+    (exp / 2)
+    |> Kernel.trunc()
+    |> Kernel.min(5000)
+    |> Kernel.max(1)
+  end
+end
diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
new file mode 100644 (file)
index 0000000..d720508
--- /dev/null
@@ -0,0 +1,227 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RateLimiter do
+  @moduledoc """
+
+  ## Configuration
+
+  A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
+
+  * The first element: `scale` (Integer). The time scale in milliseconds.
+  * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
+
+  It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
+
+  To disable a limiter set its value to `nil`.
+
+  ### Example
+
+      config :pleroma, :rate_limit,
+        one: {1000, 10},
+        two: [{10_000, 10}, {10_000, 50}],
+        foobar: nil
+
+  Here we have three limiters:
+
+  * `one` which is not over 10req/1s
+  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
+  * `foobar` which is disabled
+
+  ## Usage
+
+  AllowedSyntax:
+
+      plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
+      plug(Pleroma.Plugs.RateLimiter, options)   # :name is a required option
+
+  Allowed options:
+
+      * `name` required, always used to fetch the limit values from the config
+      * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
+      * `params` appends values of specified request params (e.g. ["id"]) to bucket name
+
+  Inside a controller:
+
+      plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
+      plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
+
+      plug(
+        Pleroma.Plugs.RateLimiter,
+        [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+        when action in ~w(fav_status unfav_status)a
+      )
+
+  or inside a router pipeline:
+
+      pipeline :api do
+        ...
+        plug(Pleroma.Plugs.RateLimiter, name: :one)
+        ...
+      end
+  """
+  import Pleroma.Web.TranslationHelpers
+  import Plug.Conn
+
+  alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
+  alias Pleroma.User
+
+  def init(opts) do
+    limiter_name = Keyword.get(opts, :name)
+
+    case Pleroma.Config.get([:rate_limit, limiter_name]) do
+      nil ->
+        nil
+
+      config ->
+        name_root = Keyword.get(opts, :bucket_name, limiter_name)
+
+        %{
+          name: name_root,
+          limits: config,
+          opts: opts
+        }
+    end
+  end
+
+  # Do not limit if there is no limiter configuration
+  def call(conn, nil), do: conn
+
+  def call(conn, settings) do
+    settings
+    |> incorporate_conn_info(conn)
+    |> check_rate()
+    |> case do
+      {:ok, _count} ->
+        conn
+
+      {:error, _count} ->
+        render_throttled_error(conn)
+    end
+  end
+
+  def inspect_bucket(conn, name_root, settings) do
+    settings =
+      settings
+      |> incorporate_conn_info(conn)
+
+    bucket_name = make_bucket_name(%{settings | name: name_root})
+    key_name = make_key_name(settings)
+    limit = get_limits(settings)
+
+    case Cachex.get(bucket_name, key_name) do
+      {:error, :no_cache} ->
+        {:err, :not_found}
+
+      {:ok, nil} ->
+        {0, limit}
+
+      {:ok, value} ->
+        {value, limit - value}
+    end
+  end
+
+  defp check_rate(settings) do
+    bucket_name = make_bucket_name(settings)
+    key_name = make_key_name(settings)
+    limit = get_limits(settings)
+
+    case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
+      {:commit, value} ->
+        {:ok, value}
+
+      {:ignore, value} ->
+        {:error, value}
+
+      {:error, :no_cache} ->
+        initialize_buckets(settings)
+        check_rate(settings)
+    end
+  end
+
+  defp increment_value(nil, _limit), do: {:commit, 1}
+
+  defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
+
+  defp increment_value(val, _limit), do: {:commit, val + 1}
+
+  defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
+    Map.merge(settings, %{
+      mode: :user,
+      conn_params: params,
+      conn_info: "#{user_id}"
+    })
+  end
+
+  defp incorporate_conn_info(settings, %{params: params} = conn) do
+    Map.merge(settings, %{
+      mode: :anon,
+      conn_params: params,
+      conn_info: "#{ip(conn)}"
+    })
+  end
+
+  defp ip(%{remote_ip: remote_ip}) do
+    remote_ip
+    |> Tuple.to_list()
+    |> Enum.join(".")
+  end
+
+  defp render_throttled_error(conn) do
+    conn
+    |> render_error(:too_many_requests, "Throttled")
+    |> halt()
+  end
+
+  defp make_key_name(settings) do
+    ""
+    |> attach_params(settings)
+    |> attach_identity(settings)
+  end
+
+  defp get_scale(_, {scale, _}), do: scale
+
+  defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
+
+  defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
+
+  defp get_limits(%{limits: {_scale, limit}}), do: limit
+
+  defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
+
+  defp get_limits(%{limits: [{_, limit}, _]}), do: limit
+
+  defp make_bucket_name(%{mode: :user, name: name_root}),
+    do: user_bucket_name(name_root)
+
+  defp make_bucket_name(%{mode: :anon, name: name_root}),
+    do: anon_bucket_name(name_root)
+
+  defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
+    param_string =
+      opts
+      |> Keyword.get(:params, [])
+      |> Enum.sort()
+      |> Enum.map(&Map.get(conn_params, &1, ""))
+      |> Enum.join(":")
+
+    "#{input}#{param_string}"
+  end
+
+  defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
+
+  defp initialize_buckets(%{name: name, limits: limits}) do
+    LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
+    LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
+  end
+
+  defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
+    do: "user:#{base}:#{conn_info}"
+
+  defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
+    do: "ip:#{base}:#{conn_info}"
+
+  defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
+  defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
+end
diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex
new file mode 100644 (file)
index 0000000..9672f78
--- /dev/null
@@ -0,0 +1,16 @@
+defmodule Pleroma.Plugs.RateLimiter.Supervisor do
+  use Supervisor
+
+  def start_link(opts) do
+    Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
+  end
+
+  def init(_args) do
+    children = [
+      Pleroma.Plugs.RateLimiter.LimiterSupervisor
+    ]
+
+    opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
+    Supervisor.init(children, opts)
+  end
+end
diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex
new file mode 100644 (file)
index 0000000..b3fb3c5
--- /dev/null
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.StaticFEPlug do
+  import Plug.Conn
+  alias Pleroma.Web.StaticFE.StaticFEController
+
+  def init(options), do: options
+
+  def call(conn, _) do
+    if enabled?() and accepts_html?(conn) do
+      conn
+      |> StaticFEController.call(:show)
+      |> halt()
+    else
+      conn
+    end
+  end
+
+  defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
+
+  defp accepts_html?(conn) do
+    conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html")
+  end
+end
index 73fad519ecbe1259f3ab0782beaddbf818943bcc..5b01b964b8a52fb85aa05a60de590df627a9f87a 100644 (file)
@@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   @relations [:follow, :unfollow]
   @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
 
-  plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
-  plug(RateLimiter, :relations_actions when action in @relations)
-  plug(RateLimiter, :app_account_creation when action == :create)
+  plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
+  plug(RateLimiter, [name: :relations_actions] when action in @relations)
+  plug(RateLimiter, [name: :app_account_creation] when action == :create)
   plug(:assign_account_by_id when action in @needs_account)
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
index bfd5120ba4bfb2ffa38cf2827c3ade35d5cced20..d9e51de7f27757c54d2ea2701669bec692052517 100644 (file)
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
 
   @local_mastodon_name "Mastodon-Local"
 
-  plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
+  plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
 
   @doc "GET /web/login"
   def login(%{assigns: %{user: %User{}}} = conn, _params) do
index 6cfd68a84b0d60e16fd2c3bbb6a0b42ee7e5f0ff..0a929f55b9137aa87ff57fdbfab750a68cec7c59 100644 (file)
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
 
   plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
 
-  plug(RateLimiter, :search when action in [:search, :search2, :account_search])
+  plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
 
   def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
     accounts = User.search(query, search_options(params, user))
index e5d016f63711dc0f899172448ddbef138a5f4cbb..74b223cf4efcfa1771c999c364bf80bcc808aae0 100644 (file)
@@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
 
   plug(
     RateLimiter,
-    {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+    [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
     when action in ~w(reblog unreblog)a
   )
 
   plug(
     RateLimiter,
-    {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+    [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
     when action in ~w(favourite unfavourite)a
   )
 
-  plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+  plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
index 6ed181cffb78e43db51ba626837e482a086b10f0..358600e7d1ff34c0bc308fb80f4c831b6e10a459 100644 (file)
@@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
   alias Pleroma.Repo
   alias Pleroma.User
 
-  plug(RateLimiter, :authentication when action in [:user_exists, :check_password])
-  plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password)
+  plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
+  plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
 
   def user_exists(conn, %{"user" => username}) do
     with %User{} <- Repo.get_by(User, nickname: username, local: true) do
index d7ae503f60ec16dc0db7e126a349f476fdaeceaf..486b9f6a4dce7d685a92e08ea42de1610f7445f5 100644 (file)
@@ -46,6 +46,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
 
         data
         |> Map.merge(%{quarantined_instances: quarantined})
+        |> Map.put(:enabled, Config.get([:instance, :federating]))
       else
         %{}
       end
index fe71aca8cea95f26f5523a439674ce620aa30cbd..2aee8cab2bab4c384f205c04a96c499764514e97 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   use Pleroma.Web, :controller
 
   alias Pleroma.Helpers.UriHelper
+  alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Registration
   alias Pleroma.Repo
   alias Pleroma.User
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
 
   plug(:fetch_session)
   plug(:fetch_flash)
-  plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization)
+  plug(RateLimiter, [name: :authentication] when action == :create_authorization)
 
   action_fallback(Pleroma.Web.OAuth.FallbackController)
 
@@ -36,7 +37,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     authorize(conn, Map.merge(params, auth_attrs))
   end
 
-  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
+  def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
     if ControllerHelper.truthy_param?(params["force_login"]) do
       do_authorize(conn, params)
     else
@@ -44,6 +45,22 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     end
   end
 
+  # Note: the token is set in oauth_plug, but the token and client do not always go together.
+  # For example, MastodonFE's token is set if user requests with another client,
+  # after user already authorized to MastodonFE.
+  # So we have to check client and token.
+  def authorize(
+        %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
+        %{"client_id" => client_id} = params
+      ) do
+    with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
+         ^client_id <- t.app.client_id do
+      handle_existing_authorization(conn, params)
+    else
+      _ -> do_authorize(conn, params)
+    end
+  end
+
   def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
 
   defp do_authorize(%Plug.Conn{} = conn, params) do
index 6958519de05d95acfadeecd749af7a52ec82d778..12a7c2365446e8ba67754b3de17af8a25bc14cd7 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Fallback.RedirectController
   alias Pleroma.Activity
   alias Pleroma.Object
+  alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPubController
   alias Pleroma.Web.ActivityPub.ObjectView
@@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Pleroma.Web.Router
 
   plug(
-    Pleroma.Plugs.RateLimiter,
-    {:ap_routes, params: ["uuid"]} when action in [:object, :activity]
+    RateLimiter,
+    [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
   )
 
   plug(
index db6faac835d2c4166e362cb1289db91127b59b98..bc2f1017c83fb52f9a3b5e1e025f835d9c2fcc99 100644 (file)
@@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
     when action != :confirmation_resend
   )
 
-  plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
+  plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
   plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
   plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
 
index 7349f4cdabee4e61dbec69d828b9267f8babcf6f..93e7e45f9ae9a7473017c023016a2b3bcb748c27 100644 (file)
@@ -503,6 +503,7 @@ defmodule Pleroma.Web.Router do
 
   pipeline :ostatus do
     plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
+    plug(Pleroma.Plugs.StaticFEPlug)
   end
 
   pipeline :oembed do
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
new file mode 100644 (file)
index 0000000..8ccf15f
--- /dev/null
@@ -0,0 +1,163 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StaticFE.StaticFEController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.Visibility
+  alias Pleroma.Web.Metadata
+  alias Pleroma.Web.Router.Helpers
+
+  plug(:put_layout, :static_fe)
+  plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
+  plug(:assign_id)
+
+  @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
+
+  defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
+    do: name
+
+  defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
+    do: summary
+
+  defp get_title(_), do: nil
+
+  defp not_found(conn, message) do
+    conn
+    |> put_status(404)
+    |> render("error.html", %{message: message, meta: ""})
+  end
+
+  def get_counts(%Activity{} = activity) do
+    %Object{data: data} = Object.normalize(activity)
+
+    %{
+      likes: data["like_count"] || 0,
+      replies: data["repliesCount"] || 0,
+      announces: data["announcement_count"] || 0
+    }
+  end
+
+  def represent(%Activity{} = activity), do: represent(activity, false)
+
+  def represent(%Activity{object: %Object{data: data}} = activity, selected) do
+    {:ok, user} = User.get_or_fetch(activity.object.data["actor"])
+
+    link =
+      case user.local do
+        true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+        _ -> data["url"] || data["external_url"] || data["id"]
+      end
+
+    %{
+      user: user,
+      title: get_title(activity.object),
+      content: data["content"] || nil,
+      attachment: data["attachment"],
+      link: link,
+      published: data["published"],
+      sensitive: data["sensitive"],
+      selected: selected,
+      counts: get_counts(activity),
+      id: activity.id
+    }
+  end
+
+  def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
+    with %Activity{local: true} = activity <-
+           Activity.get_by_id_with_object(notice_id),
+         true <- Visibility.is_public?(activity.object),
+         %User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
+      meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
+
+      timeline =
+        activity.object.data["context"]
+        |> ActivityPub.fetch_activities_for_context(%{})
+        |> Enum.reverse()
+        |> Enum.map(&represent(&1, &1.object.id == activity.object.id))
+
+      render(conn, "conversation.html", %{activities: timeline, meta: meta})
+    else
+      %Activity{object: %Object{data: data}} ->
+        conn
+        |> put_status(:found)
+        |> redirect(external: data["url"] || data["external_url"] || data["id"])
+
+      _ ->
+        not_found(conn, "Post not found.")
+    end
+  end
+
+  def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
+    case User.get_cached_by_nickname_or_id(username_or_id) do
+      %User{} = user ->
+        meta = Metadata.build_tags(%{user: user})
+
+        timeline =
+          ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
+          |> Enum.map(&represent/1)
+
+        prev_page_id =
+          (params["min_id"] || params["max_id"]) &&
+            List.first(timeline) && List.first(timeline).id
+
+        next_page_id = List.last(timeline) && List.last(timeline).id
+
+        render(conn, "profile.html", %{
+          user: user,
+          timeline: timeline,
+          prev_page_id: prev_page_id,
+          next_page_id: next_page_id,
+          meta: meta
+        })
+
+      _ ->
+        not_found(conn, "User not found.")
+    end
+  end
+
+  def show(%{assigns: %{object_id: _}} = conn, _params) do
+    url = Helpers.url(conn) <> conn.request_path
+
+    case Activity.get_create_by_object_ap_id_with_object(url) do
+      %Activity{} = activity ->
+        to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
+        redirect(conn, to: to)
+
+      _ ->
+        not_found(conn, "Post not found.")
+    end
+  end
+
+  def show(%{assigns: %{activity_id: _}} = conn, _params) do
+    url = Helpers.url(conn) <> conn.request_path
+
+    case Activity.get_by_ap_id(url) do
+      %Activity{} = activity ->
+        to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
+        redirect(conn, to: to)
+
+      _ ->
+        not_found(conn, "Post not found.")
+    end
+  end
+
+  def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
+    do: assign(conn, :notice_id, notice_id)
+
+  def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
+    do: assign(conn, :username_or_id, user_id)
+
+  def assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
+    do: assign(conn, :object_id, object_id)
+
+  def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
+    do: assign(conn, :activity_id, activity_id)
+
+  def assign_id(conn, _opts), do: conn
+end
diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex
new file mode 100644 (file)
index 0000000..821ece9
--- /dev/null
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.StaticFE.StaticFEView do
+  use Pleroma.Web, :view
+
+  alias Calendar.Strftime
+  alias Pleroma.Emoji.Formatter
+  alias Pleroma.User
+  alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Gettext
+  alias Pleroma.Web.MediaProxy
+  alias Pleroma.Web.Metadata.Utils
+  alias Pleroma.Web.Router.Helpers
+
+  use Phoenix.HTML
+
+  @media_types ["image", "audio", "video"]
+
+  def emoji_for_user(%User{} = user) do
+    user.source_data
+    |> Map.get("tag", [])
+    |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
+    |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+      {String.trim(name, ":"), url}
+    end)
+  end
+
+  def fetch_media_type(%{"mediaType" => mediaType}) do
+    Utils.fetch_media_type(@media_types, mediaType)
+  end
+
+  def format_date(date) do
+    {:ok, date, _} = DateTime.from_iso8601(date)
+    Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC")
+  end
+
+  def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma")
+
+  def open_content? do
+    Pleroma.Config.get(
+      [:frontend_configurations, :collapse_message_with_subjects],
+      true
+    )
+  end
+end
diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex
new file mode 100644 (file)
index 0000000..819632c
--- /dev/null
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
+    <title><%= Pleroma.Config.get([:instance, :name]) %></title>
+    <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
+    <link rel="stylesheet" href="/static/static-fe.css">
+  </head>
+  <body>
+    <div class="container">
+      <%= render @view_module, @view_template, assigns %>
+    </div>
+  </body>
+</html>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex
new file mode 100644 (file)
index 0000000..7e04e95
--- /dev/null
@@ -0,0 +1,8 @@
+<%= case @mediaType do %>
+<% "audio" -> %>
+<audio src="<%= @url %>" controls="controls"></audio>
+<% "video" -> %>
+<video src="<%= @url %>" controls="controls"></video>
+<% _ -> %>
+<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
+<% end %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex
new file mode 100644 (file)
index 0000000..df5e5ee
--- /dev/null
@@ -0,0 +1,37 @@
+<div class="activity" <%= if @selected do %> id="selected" <% end %>>
+  <p class="pull-right">
+    <%= link format_date(@published), to: @link, class: "activity-link" %>
+  </p>
+  <%= render("_user_card.html", %{user: @user}) %>
+  <div class="activity-content">
+    <%= if @title != "" do %>
+      <details <%= if open_content?() do %>open<% end %>>
+        <summary><%= raw @title %></summary>
+        <div class="e-content"><%= raw @content %></div>
+      </details>
+    <% else %>
+      <div class="e-content"><%= raw @content %></div>
+    <% end %>
+    <%= for %{"name" => name, "url" => [url | _]} <- @attachment do %>
+      <%= if @sensitive do %>
+        <details class="nsfw">
+          <summary><%= Gettext.gettext("sensitive media") %></summary>
+          <div>
+            <%= render("_attachment.html", %{name: name, url: url["href"],
+                                             mediaType: fetch_media_type(url)}) %>
+          </div>
+        </details>
+      <% else %>
+        <%= render("_attachment.html", %{name: name, url: url["href"],
+                                         mediaType: fetch_media_type(url)}) %>
+      <% end %>
+    <% end %>
+  </div>
+  <%= if @selected do %>
+    <dl class="counts">
+      <dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
+      <dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
+      <dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
+    </dl>
+  <% end %>
+</div>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex
new file mode 100644 (file)
index 0000000..c7789f9
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="p-author h-card">
+  <a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>">
+    <div class="avatar">
+      <img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
+    </div>
+    <span class="display-name">
+      <bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
+      <span class="nickname"><%= @user.nickname %></span>
+    </span>
+  </a>
+</div>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex
new file mode 100644 (file)
index 0000000..2acd848
--- /dev/null
@@ -0,0 +1,11 @@
+<header>
+  <h1><%= link instance_name(), to: "/" %></h1>
+</header>
+
+<main>
+  <div class="conversation">
+    <%= for activity <- @activities do %>
+      <%= render("_notice.html", activity) %>
+    <% end %>
+  </div>
+</main>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex
new file mode 100644 (file)
index 0000000..d98a1eb
--- /dev/null
@@ -0,0 +1,7 @@
+<header>
+  <h1><%= gettext("Oops") %></h1>
+</header>
+
+<main>
+  <p><%= @message %></p>
+</main>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
new file mode 100644 (file)
index 0000000..94063c9
--- /dev/null
@@ -0,0 +1,31 @@
+<header>
+  <h1><%= link instance_name(), to: "/" %></h1>
+
+  <h3>
+    <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
+      <input type="hidden" name="nickname" value="<%= @user.nickname %>">
+      <input type="hidden" name="profile" value="">
+      <button type="submit" class="collapse">Remote follow</button>
+    </form>
+    <%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
+    <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %>
+  </h3>
+  <p><%= raw @user.bio %></p>
+</header>
+
+<main>
+  <div class="activity-stream">
+    <%= for activity <- @timeline do %>
+      <%= render("_notice.html", Map.put(activity, :selected, false)) %>
+    <% end %>
+    <p id="pagination">
+      <%= if @prev_page_id do %>
+        <%= link "«", to: "?min_id=" <> @prev_page_id %>
+      <% end %>
+      <%= if @prev_page_id && @next_page_id, do: " | " %>
+      <%= if @next_page_id do %>
+        <%= link "»", to: "?max_id=" <> @next_page_id %>
+      <% end %>
+    </p>
+  </div>
+</main>
diff --git a/mix.exs b/mix.exs
index dd7c7e979b3e03fa4376fb440b81b9437d808482..81ce4f25cef74d619776b7a7b6b18a569ff014f8 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -155,7 +155,6 @@ defmodule Pleroma.Mixfile do
       {:joken, "~> 2.0"},
       {:benchee, "~> 1.0"},
       {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
-      {:ex_rated, "~> 1.3"},
       {:ex_const, "~> 0.2"},
       {:plug_static_index_html, "~> 1.0.0"},
       {:excoveralls, "~> 0.11.1", only: :test},
index 5b471fe3dbfdb9f1d17807269224da2c35ec3b85..d4a80df77ccce408db16e0806333bfb24258bb6f 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -33,7 +33,6 @@
   "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"},
   "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
-  "ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
   "excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
   "fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"},
diff --git a/priv/static/static/static-fe.css b/priv/static/static/static-fe.css
new file mode 100644 (file)
index 0000000..19c5638
--- /dev/null
@@ -0,0 +1,176 @@
+body {
+    background-color: #282c37;
+    font-family: sans-serif;
+    color: white;
+}
+
+main {
+    margin: 50px auto;
+    max-width: 960px;
+    padding: 40px;
+    background-color: #313543;
+    border-radius: 4px;
+}
+
+header {
+    margin: 50px auto;
+    max-width: 960px;
+    padding: 40px;
+    background-color: #313543;
+    border-radius: 4px;
+}
+
+.activity {
+    border-radius: 4px;
+    padding: 1em;
+    padding-bottom: 2em;
+    margin-bottom: 1em;
+}
+
+.avatar {
+    cursor: pointer;
+}
+
+.avatar img {
+    float: left;
+    border-radius: 4px;
+    margin-right: 4px;
+}
+
+.activity-content img, video, audio {
+    padding: 1em;
+    max-width: 800px;
+    max-height: 800px;
+}
+
+#selected {
+    background-color: #1b2735;
+}
+
+.counts dt, .counts dd {
+    float: left;
+    margin-left: 1em;
+}
+
+a {
+    color: white;
+}
+
+.h-card {
+    min-height: 48px;
+    margin-bottom: 8px;
+}
+
+header a, .h-card a {
+    text-decoration: none;
+}
+
+header a:hover, .h-card a:hover {
+    text-decoration: underline;
+}
+
+.display-name {
+    padding-top: 4px;
+    display: block;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    color: white;
+}
+
+/* keep emoji from being hilariously huge */
+.display-name img {
+    max-height: 1em;
+}
+
+.display-name .nickname {
+    padding-top: 4px;
+    display: block;
+}
+
+.nickname:hover {
+    text-decoration: none;
+}
+
+.pull-right {
+    float: right;
+}
+
+.collapse {
+    margin: 0;
+    width: auto;
+}
+
+h1 {
+    margin: 0;
+}
+
+h2 {
+    color: #9baec8;
+    font-weight: normal;
+    font-size: 20px;
+    margin-bottom: 40px;
+}
+
+form {
+    width: 100%;
+}
+
+input {
+    box-sizing: border-box;
+    width: 100%;
+    padding: 10px;
+    margin-top: 20px;
+    background-color: rgba(0,0,0,.1);
+    color: white;
+    border: 0;
+    border-bottom: 2px solid #9baec8;
+    font-size: 14px;
+}
+
+input:focus {
+    border-bottom: 2px solid #4b8ed8;
+}
+
+input[type="checkbox"] {
+    width: auto;
+}
+
+button {
+    box-sizing: border-box;
+    width: 100%;
+    color: white;
+    background-color: #419bdd;
+    border-radius: 4px;
+    border: none;
+    padding: 10px;
+    margin-top: 30px;
+    text-transform: uppercase;
+    font-weight: 500;
+    font-size: 16px;
+}
+
+.alert-danger {
+    box-sizing: border-box;
+    width: 100%;
+    color: #D8000C;
+    background-color: #FFD2D2;
+    border-radius: 4px;
+    border: none;
+    padding: 10px;
+    margin-top: 20px;
+    font-weight: 500;
+    font-size: 16px;
+}
+
+.alert-info {
+    box-sizing: border-box;
+    width: 100%;
+    color: #00529B;
+    background-color: #BDE5F8;
+    border-radius: 4px;
+    border: none;
+    padding: 10px;
+    margin-top: 20px;
+    font-weight: 500;
+    font-size: 16px;
+}
index 71fe5204cbf634ce2ee96c04147df5fe5d99d900..7636803a63a97f6f313d91798d40f3ae39874770 100644 (file)
@@ -17,6 +17,16 @@ defmodule Pleroma.Object.ContainmentTest do
   end
 
   describe "general origin containment" do
+    test "works for completely actorless posts" do
+      assert :error ==
+               Containment.contain_origin("https://glaceon.social/users/monorail", %{
+                 "deleted" => "2019-10-30T05:48:50.249606Z",
+                 "formerType" => "Note",
+                 "id" => "https://glaceon.social/users/monorail/statuses/103049757364029187",
+                 "type" => "Tombstone"
+               })
+    end
+
     test "contain_origin_from_id() catches obvious spoofing attempts" do
       data = %{
         "id" => "http://example.com/~alyssa/activities/1234.json"
index 395095079f1346b377af6e251a2ebab54cb3fdea..49f63c424aa08079479646bdff8ae5b0a7802f2c 100644 (file)
@@ -12,163 +12,196 @@ defmodule Pleroma.Plugs.RateLimiterTest do
 
   # Note: each example must work with separate buckets in order to prevent concurrency issues
 
-  test "init/1" do
-    limiter_name = :test_init
-    Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
+  describe "config" do
+    test "config is required for plug to work" do
+      limiter_name = :test_init
+      Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
 
-    assert {limiter_name, {1, 1}, []} == RateLimiter.init(limiter_name)
-    assert nil == RateLimiter.init(:foo)
-  end
+      assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
+               RateLimiter.init(name: limiter_name)
 
-  test "ip/1" do
-    assert "127.0.0.1" == RateLimiter.ip(%{remote_ip: {127, 0, 0, 1}})
-  end
+      assert nil == RateLimiter.init(name: :foo)
+    end
 
-  test "it restricts by opts" do
-    limiter_name = :test_opts
-    scale = 1000
-    limit = 5
+    test "it restricts based on config values" do
+      limiter_name = :test_opts
+      scale = 80
+      limit = 5
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+      Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
 
-    opts = RateLimiter.init(limiter_name)
-    conn = conn(:get, "/")
-    bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
+      opts = RateLimiter.init(name: limiter_name)
+      conn = conn(:get, "/")
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+        Process.sleep(10)
+      end
 
-    conn = RateLimiter.call(conn, opts)
-    assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = RateLimiter.call(conn, opts)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
 
-    conn = RateLimiter.call(conn, opts)
-    assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      Process.sleep(50)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = conn(:get, "/")
 
-    conn = RateLimiter.call(conn, opts)
-    assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
 
-    conn = RateLimiter.call(conn, opts)
+      refute conn.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn.resp_body
+      refute conn.halted
+    end
+  end
 
-    assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
-    assert conn.halted
+  describe "options" do
+    test "`bucket_name` option overrides default bucket name" do
+      limiter_name = :test_bucket_name
 
-    Process.sleep(to_reset)
+      Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
 
-    conn = conn(:get, "/")
+      base_bucket_name = "#{limiter_name}:group1"
+      opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = conn(:get, "/")
 
-    refute conn.status == Plug.Conn.Status.code(:too_many_requests)
-    refute conn.resp_body
-    refute conn.halted
-  end
+      RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
+      assert {:err, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+    end
 
-  test "`bucket_name` option overrides default bucket name" do
-    limiter_name = :test_bucket_name
-    scale = 1000
-    limit = 5
+    test "`params` option allows different queries to be tracked independently" do
+      limiter_name = :test_params
+      Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
-    base_bucket_name = "#{limiter_name}:group1"
-    opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name})
+      opts = RateLimiter.init(name: limiter_name, params: ["id"])
 
-    conn = conn(:get, "/")
-    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
-    customized_bucket_name = "#{base_bucket_name}:#{RateLimiter.ip(conn)}"
+      conn = conn(:get, "/?id=1")
+      conn = Plug.Conn.fetch_query_params(conn)
+      conn_2 = conn(:get, "/?id=2")
 
-    RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(customized_bucket_name, scale, limit)
-    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
-  end
-
-  test "`params` option appends specified params' values to bucket name" do
-    limiter_name = :test_params
-    scale = 1000
-    limit = 5
+      RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+      assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+    end
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
-    opts = RateLimiter.init({limiter_name, params: ["id"]})
-    id = "1"
+    test "it supports combination of options modifying bucket name" do
+      limiter_name = :test_options_combo
+      Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
 
-    conn = conn(:get, "/?id=#{id}")
-    conn = Plug.Conn.fetch_query_params(conn)
+      base_bucket_name = "#{limiter_name}:group1"
+      opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
+      id = "100"
 
-    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
-    parametrized_bucket_name = "#{limiter_name}:#{id}:#{RateLimiter.ip(conn)}"
+      conn = conn(:get, "/?id=#{id}")
+      conn = Plug.Conn.fetch_query_params(conn)
+      conn_2 = conn(:get, "/?id=#{101}")
 
-    RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
-    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+      RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
+      assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, opts)
+    end
   end
 
-  test "it supports combination of options modifying bucket name" do
-    limiter_name = :test_options_combo
-    scale = 1000
-    limit = 5
+  describe "unauthenticated users" do
+    test "are restricted based on remote IP" do
+      limiter_name = :test_unauthenticated
+      Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
+
+      opts = RateLimiter.init(name: limiter_name)
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
-    base_bucket_name = "#{limiter_name}:group1"
-    opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name, params: ["id"]})
-    id = "100"
+      conn = %{conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
+      conn_2 = %{conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
 
-    conn = conn(:get, "/?id=#{id}")
-    conn = Plug.Conn.fetch_query_params(conn)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+        refute conn.halted
+      end
 
-    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
-    parametrized_bucket_name = "#{base_bucket_name}:#{id}:#{RateLimiter.ip(conn)}"
+      conn = RateLimiter.call(conn, opts)
 
-    RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
-    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
+
+      conn_2 = RateLimiter.call(conn_2, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+
+      refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn_2.resp_body
+      refute conn_2.halted
+    end
   end
 
-  test "optional limits for authenticated users" do
-    limiter_name = :test_authenticated
-    Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
+  describe "authenticated users" do
+    setup do
+      Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
+
+      :ok
+    end
+
+    test "can have limits seperate from unauthenticated connections" do
+      limiter_name = :test_authenticated
+
+      scale = 1000
+      limit = 5
+      Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
+
+      opts = RateLimiter.init(name: limiter_name)
 
-    scale = 1000
-    limit = 5
-    Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
+      user = insert(:user)
+      conn = conn(:get, "/") |> assign(:user, user)
 
-    opts = RateLimiter.init(limiter_name)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+        refute conn.halted
+      end
 
-    user = insert(:user)
-    conn = conn(:get, "/") |> assign(:user, user)
-    bucket_name = "#{limiter_name}:#{user.id}"
+      conn = RateLimiter.call(conn, opts)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
 
-    conn = RateLimiter.call(conn, opts)
-    assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      Process.sleep(1550)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = conn(:get, "/") |> assign(:user, user)
+      conn = RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      refute conn.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn.resp_body
+      refute conn.halted
+    end
 
-    conn = RateLimiter.call(conn, opts)
-    assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+    test "diffrerent users are counted independently" do
+      limiter_name = :test_authenticated
+      Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
 
-    conn = RateLimiter.call(conn, opts)
+      opts = RateLimiter.init(name: limiter_name)
 
-    assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
-    assert conn.halted
+      user = insert(:user)
+      conn = conn(:get, "/") |> assign(:user, user)
 
-    Process.sleep(to_reset)
+      user_2 = insert(:user)
+      conn_2 = conn(:get, "/") |> assign(:user, user_2)
 
-    conn = conn(:get, "/") |> assign(:user, user)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+      end
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = RateLimiter.call(conn, opts)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
 
-    refute conn.status == Plug.Conn.Status.code(:too_many_requests)
-    refute conn.resp_body
-    refute conn.halted
+      conn_2 = RateLimiter.call(conn_2, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+      refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn_2.resp_body
+      refute conn_2.halted
+    end
   end
 end
index 2a9e4f5a01783a774f20573015702cb2078d18b3..bc92353091fed0216a4da9ca348ed326ac14b7c0 100644 (file)
@@ -2269,6 +2269,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       Pleroma.Config.put([:instance, :dynamic_configuration], true)
     end
 
+    clear_config([:feed, :post_title]) do
+      Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"})
+    end
+
     test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
       assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
       conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
index a3281b25b7235036797bffd055ebce706d41be2e..6cc8766020d02f4943a8ca71b4dd8c6083878f90 100644 (file)
@@ -84,6 +84,30 @@ defmodule Pleroma.Web.NodeInfoTest do
     Pleroma.Config.put([:instance, :safe_dm_mentions], option)
   end
 
+  test "it shows if federation is enabled/disabled", %{conn: conn} do
+    original = Pleroma.Config.get([:instance, :federating])
+
+    Pleroma.Config.put([:instance, :federating], true)
+
+    response =
+      conn
+      |> get("/nodeinfo/2.1.json")
+      |> json_response(:ok)
+
+    assert response["metadata"]["federation"]["enabled"] == true
+
+    Pleroma.Config.put([:instance, :federating], false)
+
+    response =
+      conn
+      |> get("/nodeinfo/2.1.json")
+      |> json_response(:ok)
+
+    assert response["metadata"]["federation"]["enabled"] == false
+
+    Pleroma.Config.put([:instance, :federating], original)
+  end
+
   test "it shows MRF transparency data if enabled", %{conn: conn} do
     config = Pleroma.Config.get([:instance, :rewrite_policy])
     Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
index ad8d7908363f99f18fd213477f5b4c408a634448..beb995cd8435f5050eb77023c9fa906baa36d947 100644 (file)
@@ -469,6 +469,29 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
       assert html_response(conn, 200) =~ ~s(type="submit")
     end
 
+    test "renders authentication page if user is already authenticated but user request with another client",
+         %{
+           app: app,
+           conn: conn
+         } do
+      token = insert(:oauth_token, app_id: app.id)
+
+      conn =
+        conn
+        |> put_session(:oauth_token, token.token)
+        |> get(
+          "/oauth/authorize",
+          %{
+            "response_type" => "code",
+            "client_id" => "another_client_id",
+            "redirect_uri" => OAuthController.default_redirect_uri(app),
+            "scope" => "read"
+          }
+        )
+
+      assert html_response(conn, 200) =~ ~s(type="submit")
+    end
+
     test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
          %{
            app: app,
diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs
new file mode 100644 (file)
index 0000000..2ce8f9f
--- /dev/null
@@ -0,0 +1,210 @@
+defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
+  use Pleroma.Web.ConnCase
+  alias Pleroma.Activity
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  clear_config_all([:static_fe, :enabled]) do
+    Pleroma.Config.put([:static_fe, :enabled], true)
+  end
+
+  describe "user profile page" do
+    test "just the profile as HTML", %{conn: conn} do
+      user = insert(:user)
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/users/#{user.nickname}")
+
+      assert html_response(conn, 200) =~ user.nickname
+    end
+
+    test "renders json unless there's an html accept header", %{conn: conn} do
+      user = insert(:user)
+
+      conn =
+        conn
+        |> put_req_header("accept", "application/json")
+        |> get("/users/#{user.nickname}")
+
+      assert json_response(conn, 200)
+    end
+
+    test "404 when user not found", %{conn: conn} do
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/users/limpopo")
+
+      assert html_response(conn, 404) =~ "not found"
+    end
+
+    test "profile does not include private messages", %{conn: conn} do
+      user = insert(:user)
+      CommonAPI.post(user, %{"status" => "public"})
+      CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/users/#{user.nickname}")
+
+      html = html_response(conn, 200)
+
+      assert html =~ ">public<"
+      refute html =~ ">private<"
+    end
+
+    test "pagination", %{conn: conn} do
+      user = insert(:user)
+      Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/users/#{user.nickname}")
+
+      html = html_response(conn, 200)
+
+      assert html =~ ">test30<"
+      assert html =~ ">test11<"
+      refute html =~ ">test10<"
+      refute html =~ ">test1<"
+    end
+
+    test "pagination, page 2", %{conn: conn} do
+      user = insert(:user)
+      activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
+      {:ok, a11} = Enum.at(activities, 11)
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/users/#{user.nickname}?max_id=#{a11.id}")
+
+      html = html_response(conn, 200)
+
+      assert html =~ ">test1<"
+      assert html =~ ">test10<"
+      refute html =~ ">test20<"
+      refute html =~ ">test29<"
+    end
+  end
+
+  describe "notice rendering" do
+    test "single notice page", %{conn: conn} do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"})
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/notice/#{activity.id}")
+
+      html = html_response(conn, 200)
+      assert html =~ "<header>"
+      assert html =~ user.nickname
+      assert html =~ "testing a thing!"
+    end
+
+    test "shows the whole thread", %{conn: conn} do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "space: the final frontier"})
+
+      CommonAPI.post(user, %{
+        "status" => "these are the voyages or something",
+        "in_reply_to_status_id" => activity.id
+      })
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/notice/#{activity.id}")
+
+      html = html_response(conn, 200)
+      assert html =~ "the final frontier"
+      assert html =~ "voyages"
+    end
+
+    test "redirect by AP object ID", %{conn: conn} do
+      user = insert(:user)
+
+      {:ok, %Activity{data: %{"object" => object_url}}} =
+        CommonAPI.post(user, %{"status" => "beam me up"})
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get(URI.parse(object_url).path)
+
+      assert html_response(conn, 302) =~ "redirected"
+    end
+
+    test "redirect by activity ID", %{conn: conn} do
+      user = insert(:user)
+
+      {:ok, %Activity{data: %{"id" => id}}} =
+        CommonAPI.post(user, %{"status" => "I'm a doctor, not a devops!"})
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get(URI.parse(id).path)
+
+      assert html_response(conn, 302) =~ "redirected"
+    end
+
+    test "404 when notice not found", %{conn: conn} do
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/notice/88c9c317")
+
+      assert html_response(conn, 404) =~ "not found"
+    end
+
+    test "404 for private status", %{conn: conn} do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"})
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/notice/#{activity.id}")
+
+      assert html_response(conn, 404) =~ "not found"
+    end
+
+    test "302 for remote cached status", %{conn: conn} do
+      user = insert(:user)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "to" => user.follower_address,
+        "cc" => "https://www.w3.org/ns/activitystreams#Public",
+        "type" => "Create",
+        "object" => %{
+          "content" => "blah blah blah",
+          "type" => "Note",
+          "attributedTo" => user.ap_id,
+          "inReplyTo" => nil
+        },
+        "actor" => user.ap_id
+      }
+
+      assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+      conn =
+        conn
+        |> put_req_header("accept", "text/html")
+        |> get("/notice/#{activity.id}")
+
+      assert html_response(conn, 302) =~ "redirected"
+    end
+  end
+end