mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 09:56:02 +03:00
Reapply 3918 (#3940)
* Reapply "Pages shield (#3918)"
This reverts commit 33b5c10654
.
* Make the FF check work against the site actor
This commit is contained in:
parent
9989ce6927
commit
2fae0146a4
@ -2,6 +2,8 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
- Pages Block List in Site Settings
|
||||
- Add `conversion_rate` to Stats API Timeseries and on the main graph
|
||||
- Add `total_conversions` and `conversion_rate` to `visitors.csv` in a goal-filtered CSV export
|
||||
- Ability to display total conversions (with a goal filter) on the main graph
|
||||
- Add `conversion_rate` to Stats API Timeseries and on the main graph
|
||||
|
@ -77,6 +77,21 @@ defmodule Plausible.Application do
|
||||
interval: :timer.seconds(35),
|
||||
warmer_fn: :refresh_updated_recently
|
||||
]},
|
||||
{Plausible.Shield.PageRuleCache, ttl_check_interval: false, ets_options: [:bag]},
|
||||
{Plausible.Cache.Warmer,
|
||||
[
|
||||
child_name: Plausible.Shield.PageRuleCache.All,
|
||||
cache_impl: Plausible.Shield.PageRuleCache,
|
||||
interval: :timer.minutes(3) + Enum.random(1..:timer.seconds(10)),
|
||||
warmer_fn: :refresh_all
|
||||
]},
|
||||
{Plausible.Cache.Warmer,
|
||||
[
|
||||
child_name: Plausible.Shield.PageRuleCache.RecentlyUpdated,
|
||||
cache_impl: Plausible.Shield.PageRuleCache,
|
||||
interval: :timer.seconds(35),
|
||||
warmer_fn: :refresh_updated_recently
|
||||
]},
|
||||
{Plausible.Auth.TOTP.Vault, key: totp_vault_key()},
|
||||
PlausibleWeb.Endpoint,
|
||||
{Oban, Application.get_env(:plausible, Oban)},
|
||||
|
@ -29,6 +29,7 @@ defmodule Plausible.Ingestion.Event do
|
||||
| :dc_ip
|
||||
| :site_ip_blocklist
|
||||
| :site_country_blocklist
|
||||
| :site_page_blocklist
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
domain: String.t() | nil,
|
||||
@ -120,6 +121,7 @@ defmodule Plausible.Ingestion.Event do
|
||||
[
|
||||
&drop_datacenter_ip/1,
|
||||
&drop_shield_rule_ip/1,
|
||||
&drop_shield_rule_page/1,
|
||||
&put_geolocation/1,
|
||||
&drop_shield_rule_country/1,
|
||||
&put_user_agent/1,
|
||||
@ -182,15 +184,18 @@ defmodule Plausible.Ingestion.Event do
|
||||
end
|
||||
|
||||
defp drop_shield_rule_ip(%__MODULE__{} = event) do
|
||||
domain = event.domain
|
||||
address = event.request.remote_ip
|
||||
if Plausible.Shields.ip_blocked?(event.domain, event.request.remote_ip) do
|
||||
drop(event, :site_ip_blocklist)
|
||||
else
|
||||
event
|
||||
end
|
||||
end
|
||||
|
||||
case Plausible.Shield.IPRuleCache.get({domain, address}) do
|
||||
%Plausible.Shield.IPRule{action: :deny} ->
|
||||
drop(event, :site_ip_blocklist)
|
||||
|
||||
_ ->
|
||||
event
|
||||
defp drop_shield_rule_page(%__MODULE__{} = event) do
|
||||
if Plausible.Shields.page_blocked?(event.domain, event.request.pathname) do
|
||||
drop(event, :site_page_blocklist)
|
||||
else
|
||||
event
|
||||
end
|
||||
end
|
||||
|
||||
@ -263,12 +268,10 @@ defmodule Plausible.Ingestion.Event do
|
||||
%__MODULE__{domain: domain, clickhouse_session_attrs: %{country_code: cc}} = event
|
||||
)
|
||||
when is_binary(domain) and is_binary(cc) do
|
||||
case Plausible.Shield.CountryRuleCache.get({domain, String.upcase(cc)}) do
|
||||
%Plausible.Shield.CountryRule{action: :deny} ->
|
||||
drop(event, :site_country_blocklist)
|
||||
|
||||
_ ->
|
||||
event
|
||||
if Plausible.Shields.country_blocked?(domain, cc) do
|
||||
drop(event, :site_country_blocklist)
|
||||
else
|
||||
event
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -24,7 +24,7 @@ defmodule Plausible.Shield.CountryRule do
|
||||
|> cast(attrs, [:site_id, :country_code])
|
||||
|> validate_required([:site_id, :country_code])
|
||||
|> validate_length(:country_code, is: 2)
|
||||
|> Ecto.Changeset.validate_change(:country_code, fn :country_code, cc ->
|
||||
|> validate_change(:country_code, fn :country_code, cc ->
|
||||
if cc in Enum.map(Location.Country.all(), & &1.alpha_2) do
|
||||
[]
|
||||
else
|
||||
|
85
lib/plausible/shield/page_rule.ex
Normal file
85
lib/plausible/shield/page_rule.ex
Normal file
@ -0,0 +1,85 @@
|
||||
defmodule CompiledRegex do
|
||||
@moduledoc """
|
||||
Ensures that the regex is compiled on load
|
||||
"""
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :string
|
||||
|
||||
def cast(val) when is_binary(val), do: {:ok, val}
|
||||
def cast(_), do: :error
|
||||
|
||||
def load(val), do: {:ok, Regex.compile!(val)}
|
||||
def dump(val), do: {:ok, val}
|
||||
end
|
||||
|
||||
defmodule Plausible.Shield.PageRule do
|
||||
@moduledoc """
|
||||
Schema for Pages block list
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@type t() :: %__MODULE__{}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "shield_rules_page" do
|
||||
belongs_to :site, Plausible.Site
|
||||
field :page_path, :string
|
||||
field :page_path_pattern, CompiledRegex
|
||||
field :action, Ecto.Enum, values: [:deny, :allow], default: :deny
|
||||
field :added_by, :string
|
||||
|
||||
# If `from_cache?` is set, the struct might be incomplete - see `Plausible.Site.Shield.Rules.IP.Cache`
|
||||
field :from_cache?, :boolean, virtual: true, default: false
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(rule \\ %__MODULE__{}, attrs) do
|
||||
rule
|
||||
|> cast(attrs, [:site_id, :page_path])
|
||||
|> validate_required([:site_id, :page_path])
|
||||
|> validate_length(:page_path, max: 250)
|
||||
|> validate_change(:page_path, fn :page_path, p ->
|
||||
if not String.starts_with?(p, "/") do
|
||||
[page_path: "must start with /"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
|> store_regex()
|
||||
|> unique_constraint(:page_path_pattern,
|
||||
name: :shield_rules_page_site_id_page_path_pattern_index,
|
||||
error_key: :page_path,
|
||||
message: "rule already exists"
|
||||
)
|
||||
end
|
||||
|
||||
defp store_regex(changeset) do
|
||||
case get_field(changeset, :page_path) do
|
||||
"/" <> _ = page_path ->
|
||||
regex =
|
||||
page_path
|
||||
|> Regex.escape()
|
||||
|> String.replace("\\*\\*", ".*")
|
||||
|> String.replace("\\*", ".*")
|
||||
|
||||
regex = "^#{regex}$"
|
||||
|
||||
verify_valid_regex(changeset, regex)
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_valid_regex(changeset, regex) do
|
||||
case Regex.compile(regex) do
|
||||
{:ok, _} ->
|
||||
put_change(changeset, :page_path_pattern, regex)
|
||||
|
||||
{:error, _} ->
|
||||
add_error(changeset, :page_path, "could not compile regular expression")
|
||||
end
|
||||
end
|
||||
end
|
65
lib/plausible/shield/page_rule_cache.ex
Normal file
65
lib/plausible/shield/page_rule_cache.ex
Normal file
@ -0,0 +1,65 @@
|
||||
defmodule Plausible.Shield.PageRuleCache do
|
||||
@moduledoc """
|
||||
Allows retrieving Page Rules by domain
|
||||
"""
|
||||
alias Plausible.Shield.PageRule
|
||||
|
||||
import Ecto.Query
|
||||
use Plausible.Cache
|
||||
|
||||
@cache_name :page_blocklist_by_domain
|
||||
|
||||
@cached_schema_fields ~w(
|
||||
id
|
||||
page_path_pattern
|
||||
action
|
||||
)a
|
||||
|
||||
@impl true
|
||||
def name(), do: @cache_name
|
||||
|
||||
@impl true
|
||||
def child_id(), do: :cache_page_blocklist
|
||||
|
||||
@impl true
|
||||
def count_all() do
|
||||
Plausible.Repo.aggregate(PageRule, :count)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def base_db_query() do
|
||||
from rule in PageRule,
|
||||
inner_join: s in assoc(rule, :site),
|
||||
select: {
|
||||
s.domain,
|
||||
s.domain_changed_from,
|
||||
%{struct(rule, ^@cached_schema_fields) | from_cache?: true}
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def get_from_source(domain) do
|
||||
query =
|
||||
base_db_query()
|
||||
|> where([..., site], site.domain == ^domain)
|
||||
|
||||
case Plausible.Repo.one(query) do
|
||||
{_, _, rule} -> %PageRule{rule | from_cache?: false}
|
||||
_any -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def unwrap_cache_keys(items) do
|
||||
Enum.reduce(items, [], fn
|
||||
{domain, nil, object}, acc ->
|
||||
[{domain, object} | acc]
|
||||
|
||||
{domain, domain_changed_from, object}, acc ->
|
||||
[
|
||||
{domain, object},
|
||||
{domain_changed_from, object} | acc
|
||||
]
|
||||
end)
|
||||
end
|
||||
end
|
@ -13,11 +13,63 @@ defmodule Plausible.Shields do
|
||||
@maximum_country_rules 30
|
||||
def maximum_country_rules(), do: @maximum_country_rules
|
||||
|
||||
@maximum_page_rules 30
|
||||
def maximum_page_rules(), do: @maximum_page_rules
|
||||
|
||||
@spec list_ip_rules(Site.t() | non_neg_integer()) :: [Shield.IPRule.t()]
|
||||
def list_ip_rules(site_or_id) do
|
||||
list(Shield.IPRule, site_or_id)
|
||||
end
|
||||
|
||||
@spec ip_blocked?(Site.t() | String.t(), String.t()) :: boolean()
|
||||
def ip_blocked?(%Site{domain: domain}, address) do
|
||||
ip_blocked?(domain, address)
|
||||
end
|
||||
|
||||
def ip_blocked?(domain, address) when is_binary(domain) and is_binary(address) do
|
||||
case Shield.IPRuleCache.get({domain, address}) do
|
||||
%Shield.IPRule{action: :deny} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@spec page_blocked?(Site.t() | String.t(), String.t()) :: boolean()
|
||||
def page_blocked?(%Site{domain: domain}, address) do
|
||||
page_blocked?(domain, address)
|
||||
end
|
||||
|
||||
def page_blocked?(domain, pathname) when is_binary(domain) and is_binary(pathname) do
|
||||
page_rules = Shield.PageRuleCache.get(domain)
|
||||
|
||||
if page_rules do
|
||||
page_rules
|
||||
|> List.wrap()
|
||||
|> Enum.find_value(false, fn rule ->
|
||||
rule.action == :deny and Regex.match?(rule.page_path_pattern, pathname)
|
||||
end)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@spec country_blocked?(Site.t() | String.t(), String.t()) :: boolean()
|
||||
def country_blocked?(%Site{domain: domain}, country_code) do
|
||||
country_blocked?(domain, country_code)
|
||||
end
|
||||
|
||||
def country_blocked?(domain, country_code) when is_binary(domain) and is_binary(country_code) do
|
||||
case Shield.CountryRuleCache.get({domain, String.upcase(country_code)}) do
|
||||
%Shield.CountryRule{action: :deny} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@spec add_ip_rule(Site.t() | non_neg_integer(), map(), Keyword.t()) ::
|
||||
{:ok, Shield.IPRule.t()} | {:error, Ecto.Changeset.t()}
|
||||
def add_ip_rule(site_or_id, params, opts \\ []) do
|
||||
@ -59,6 +111,28 @@ defmodule Plausible.Shields do
|
||||
count(Shield.CountryRule, site_or_id)
|
||||
end
|
||||
|
||||
@spec list_page_rules(Site.t() | non_neg_integer()) :: [Shield.PageRule.t()]
|
||||
def list_page_rules(site_or_id) do
|
||||
list(Shield.PageRule, site_or_id)
|
||||
end
|
||||
|
||||
@spec add_page_rule(Site.t() | non_neg_integer(), map(), Keyword.t()) ::
|
||||
{:ok, Shield.PageRule.t()} | {:error, Ecto.Changeset.t()}
|
||||
def add_page_rule(site_or_id, params, opts \\ []) do
|
||||
opts = Keyword.put(opts, :limit, {:page_path, @maximum_page_rules})
|
||||
add(Shield.PageRule, site_or_id, params, opts)
|
||||
end
|
||||
|
||||
@spec remove_page_rule(Site.t() | non_neg_integer(), String.t()) :: :ok
|
||||
def remove_page_rule(site_or_id, rule_id) do
|
||||
remove(Shield.PageRule, site_or_id, rule_id)
|
||||
end
|
||||
|
||||
@spec count_page_rules(Site.t() | non_neg_integer()) :: non_neg_integer()
|
||||
def count_page_rules(site_or_id) do
|
||||
count(Shield.PageRule, site_or_id)
|
||||
end
|
||||
|
||||
defp list(schema, %Site{id: id}) do
|
||||
list(schema, id)
|
||||
end
|
||||
|
136
lib/plausible/shields/country_test.exs
Normal file
136
lib/plausible/shields/country_test.exs
Normal file
@ -0,0 +1,136 @@
|
||||
defmodule Plausible.Shields.CountryTest do
|
||||
use Plausible.DataCase
|
||||
import Plausible.Shields
|
||||
|
||||
setup do
|
||||
site = insert(:site)
|
||||
{:ok, %{site: site}}
|
||||
end
|
||||
|
||||
describe "add_country_rule/2" do
|
||||
test "no input", %{site: site} do
|
||||
assert {:error, changeset} = add_country_rule(site, %{})
|
||||
assert changeset.errors == [country_code: {"can't be blank", [validation: :required]}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "unsupported country", %{site: site} do
|
||||
assert {:error, changeset} = add_country_rule(site, %{"country_code" => "0X"})
|
||||
assert changeset.errors == [country_code: {"is invalid", []}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "incorrect country format", %{site: site} do
|
||||
assert {:error, changeset} = add_country_rule(site, %{"country_code" => "Germany"})
|
||||
|
||||
assert changeset.errors ==
|
||||
[
|
||||
{:country_code, {"is invalid", []}},
|
||||
{:country_code,
|
||||
{"should be %{count} character(s)",
|
||||
[count: 2, validation: :length, kind: :is, type: :string]}}
|
||||
]
|
||||
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "double insert", %{site: site} do
|
||||
assert {:ok, _} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
assert {:error, changeset} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
refute changeset.valid?
|
||||
|
||||
assert changeset.errors == [
|
||||
country_code:
|
||||
{"has already been taken",
|
||||
[
|
||||
{:constraint, :unique},
|
||||
{:constraint_name, "shield_rules_country_site_id_country_code_index"}
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
||||
test "over limit", %{site: site} do
|
||||
country_codes =
|
||||
Location.Country.all()
|
||||
|> Enum.take(Plausible.Shields.maximum_country_rules())
|
||||
|> Enum.map(& &1.alpha_2)
|
||||
|
||||
for cc <- country_codes do
|
||||
assert {:ok, _} =
|
||||
add_country_rule(site, %{"country_code" => cc})
|
||||
end
|
||||
|
||||
assert count_country_rules(site) == maximum_country_rules()
|
||||
|
||||
assert {:error, changeset} =
|
||||
add_country_rule(site, %{"country_code" => "US"})
|
||||
|
||||
refute changeset.valid?
|
||||
assert changeset.errors == [country_code: {"maximum reached", []}]
|
||||
end
|
||||
|
||||
test "with added_by", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_country_rule(site, %{"country_code" => "EE"},
|
||||
added_by: build(:user, name: "Joe", email: "joe@example.com")
|
||||
)
|
||||
|
||||
assert rule.added_by == "Joe <joe@example.com>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_country_rule/2" do
|
||||
test "is idempontent", %{site: site} do
|
||||
{:ok, rule} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
assert remove_country_rule(site, rule.id) == :ok
|
||||
refute Repo.get(Plausible.Shield.CountryRule, rule.id)
|
||||
assert remove_country_rule(site, rule.id) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_country_rules/1" do
|
||||
test "empty", %{site: site} do
|
||||
assert(list_country_rules(site) == [])
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "many", %{site: site} do
|
||||
{:ok, r1} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
:timer.sleep(1000)
|
||||
{:ok, r2} = add_country_rule(site, %{"country_code" => "PL"})
|
||||
assert [^r2, ^r1] = list_country_rules(site)
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_country_rules/1" do
|
||||
test "counts", %{site: site} do
|
||||
assert count_country_rules(site) == 0
|
||||
{:ok, _} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
assert count_country_rules(site) == 1
|
||||
{:ok, _} = add_country_rule(site, %{"country_code" => "PL"})
|
||||
assert count_country_rules(site) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "Country Rules" do
|
||||
test "end to end", %{site: site} do
|
||||
site2 = insert(:site)
|
||||
|
||||
assert count_country_rules(site.id) == 0
|
||||
assert list_country_rules(site.id) == []
|
||||
|
||||
assert {:ok, rule} =
|
||||
add_country_rule(site.id, %{"country_code" => "EE"})
|
||||
|
||||
add_country_rule(site2, %{"country_code" => "PL"})
|
||||
|
||||
assert count_country_rules(site) == 1
|
||||
assert [^rule] = list_country_rules(site)
|
||||
assert rule.country_code == "EE"
|
||||
assert rule.action == :deny
|
||||
refute rule.from_cache?
|
||||
assert country_blocked?(site, "ee")
|
||||
refute country_blocked?(site, "xx")
|
||||
end
|
||||
end
|
||||
end
|
147
lib/plausible/shields/ip_test.exs
Normal file
147
lib/plausible/shields/ip_test.exs
Normal file
@ -0,0 +1,147 @@
|
||||
defmodule Plausible.Shields.IPTest do
|
||||
use Plausible.DataCase
|
||||
import Plausible.Shields
|
||||
|
||||
setup do
|
||||
site = insert(:site)
|
||||
{:ok, %{site: site}}
|
||||
end
|
||||
|
||||
describe "add_ip_rule/2" do
|
||||
test "no input", %{site: site} do
|
||||
assert {:error, changeset} = add_ip_rule(site, %{})
|
||||
assert changeset.errors == [inet: {"can't be blank", [validation: :required]}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "unsupported netmask", %{site: site} do
|
||||
assert {:error, changeset} = add_ip_rule(site, %{"inet" => "127.0.0.0/24"})
|
||||
assert changeset.errors == [inet: {"netmask unsupported", []}]
|
||||
refute changeset.valid?
|
||||
assert {:ok, _} = add_ip_rule(site, %{"inet" => "127.0.0.0/32"})
|
||||
end
|
||||
|
||||
test "incorrect ip", %{site: site} do
|
||||
assert {:error, changeset} = add_ip_rule(site, %{"inet" => "999.999.999.999"})
|
||||
|
||||
assert changeset.errors == [
|
||||
inet: {"is invalid", [{:type, EctoNetwork.INET}, {:validation, :cast}]}
|
||||
]
|
||||
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "non-strict IPs", %{site: site} do
|
||||
assert {:error, _} = add_ip_rule(site, %{"inet" => "111"})
|
||||
end
|
||||
|
||||
test "double insert", %{site: site} do
|
||||
assert {:ok, _} = add_ip_rule(site, %{"inet" => "0.0.0.111"})
|
||||
assert {:error, changeset} = add_ip_rule(site, %{"inet" => "0.0.0.111"})
|
||||
refute changeset.valid?
|
||||
|
||||
assert changeset.errors == [
|
||||
inet:
|
||||
{"has already been taken",
|
||||
[
|
||||
{:constraint, :unique},
|
||||
{:constraint_name, "shield_rules_ip_site_id_inet_index"}
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
||||
test "ipv6", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site, %{"inet" => "2001:0000:130F:0000:0000:09C0:876A:130B"})
|
||||
|
||||
assert ^rule = Repo.get(Plausible.Shield.IPRule, rule.id)
|
||||
end
|
||||
|
||||
test "ipv4", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.1"})
|
||||
|
||||
assert ^rule = Repo.get(Plausible.Shield.IPRule, rule.id)
|
||||
end
|
||||
|
||||
test "over limit", %{site: site} do
|
||||
for i <- 1..maximum_ip_rules() do
|
||||
assert {:ok, _} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.#{i}"})
|
||||
end
|
||||
|
||||
assert count_ip_rules(site) == maximum_ip_rules()
|
||||
|
||||
assert {:error, changeset} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.31"})
|
||||
|
||||
refute changeset.valid?
|
||||
assert changeset.errors == [inet: {"maximum reached", []}]
|
||||
end
|
||||
|
||||
test "with added_by", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.1"},
|
||||
added_by: build(:user, name: "Joe", email: "joe@example.com")
|
||||
)
|
||||
|
||||
assert rule.added_by == "Joe <joe@example.com>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_ip_rule/2" do
|
||||
test "is idempontent", %{site: site} do
|
||||
{:ok, rule} = add_ip_rule(site, %{"inet" => "127.0.0.1"})
|
||||
assert remove_ip_rule(site, rule.id) == :ok
|
||||
refute Repo.get(Plausible.Shield.IPRule, rule.id)
|
||||
assert remove_ip_rule(site, rule.id) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_ip_rules/1" do
|
||||
test "empty", %{site: site} do
|
||||
assert(list_ip_rules(site) == [])
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "many", %{site: site} do
|
||||
{:ok, r1} = add_ip_rule(site, %{"inet" => "127.0.0.1"})
|
||||
:timer.sleep(1000)
|
||||
{:ok, r2} = add_ip_rule(site, %{"inet" => "127.0.0.2"})
|
||||
assert [^r2, ^r1] = list_ip_rules(site)
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_ip_rules/1" do
|
||||
test "counts", %{site: site} do
|
||||
assert count_ip_rules(site) == 0
|
||||
{:ok, _} = add_ip_rule(site, %{"inet" => "127.0.0.1"})
|
||||
assert count_ip_rules(site) == 1
|
||||
{:ok, _} = add_ip_rule(site, %{"inet" => "127.0.0.2"})
|
||||
assert count_ip_rules(site) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "IP Rules" do
|
||||
test "end to end", %{site: site} do
|
||||
site2 = insert(:site)
|
||||
|
||||
assert count_ip_rules(site.id) == 0
|
||||
assert list_ip_rules(site.id) == []
|
||||
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site.id, %{"inet" => "127.0.0.1", "description" => "Localhost"})
|
||||
|
||||
add_ip_rule(site2, %{"inet" => "127.0.0.1", "description" => "Localhost"})
|
||||
|
||||
assert count_ip_rules(site) == 1
|
||||
assert [^rule] = list_ip_rules(site)
|
||||
assert rule.inet == %Postgrex.INET{address: {127, 0, 0, 1}, netmask: 32}
|
||||
assert rule.description == "Localhost"
|
||||
assert rule.action == :deny
|
||||
refute rule.from_cache?
|
||||
assert ip_blocked?(site, "127.0.0.1")
|
||||
refute ip_blocked?(site, "127.0.0.2")
|
||||
end
|
||||
end
|
||||
end
|
174
lib/plausible/shields/page_test.exs
Normal file
174
lib/plausible/shields/page_test.exs
Normal file
@ -0,0 +1,174 @@
|
||||
defmodule Plausible.Shields.PageTest do
|
||||
use Plausible.DataCase
|
||||
import Plausible.Shields
|
||||
|
||||
setup do
|
||||
site = insert(:site)
|
||||
{:ok, %{site: site}}
|
||||
end
|
||||
|
||||
describe "add_page_rule/2" do
|
||||
test "no input", %{site: site} do
|
||||
assert {:error, changeset} = add_page_rule(site, %{})
|
||||
assert changeset.errors == [page_path: {"can't be blank", [validation: :required]}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "no slash", %{site: site} do
|
||||
assert {:error, changeset} = add_page_rule(site, %{"page_path" => "test"})
|
||||
assert changeset.errors == [page_path: {"must start with /", []}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "lengthy", %{site: site} do
|
||||
long = "/" <> :binary.copy("a", 251)
|
||||
assert {:error, changeset} = add_page_rule(site, %{"page_path" => long})
|
||||
assert [page_path: {"should be at most %{count} character(s)", _}] = changeset.errors
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "double insert", %{site: site} do
|
||||
assert {:ok, _} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
assert {:error, changeset} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
refute changeset.valid?
|
||||
|
||||
assert changeset.errors == [
|
||||
page_path:
|
||||
{"rule already exists",
|
||||
[
|
||||
{:constraint, :unique},
|
||||
{:constraint_name, "shield_rules_page_site_id_page_path_pattern_index"}
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
||||
test "equivalent rules are counted as dupes", %{site: site} do
|
||||
assert {:ok, _} = add_page_rule(site, %{"page_path" => "/test/*"})
|
||||
assert {:error, changeset} = add_page_rule(site, %{"page_path" => "/test/**"})
|
||||
|
||||
assert changeset.errors == [
|
||||
page_path:
|
||||
{"rule already exists",
|
||||
[
|
||||
{:constraint, :unique},
|
||||
{:constraint_name, "shield_rules_page_site_id_page_path_pattern_index"}
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
||||
test "regex storage: wildcard", %{site: site} do
|
||||
assert {:ok, rule} = add_page_rule(site, %{"page_path" => "/test/*"})
|
||||
assert rule.page_path_pattern == "^/test/.*$"
|
||||
end
|
||||
|
||||
test "regex storage: no wildcard", %{site: site} do
|
||||
assert {:ok, rule} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
assert rule.page_path_pattern == "^/test$"
|
||||
end
|
||||
|
||||
test "regex storage: escaping", %{site: site} do
|
||||
assert {:ok, rule} = add_page_rule(site, %{"page_path" => "/test/*/**/|+[0-9]"})
|
||||
assert rule.page_path_pattern == "^/test/.*/.*/\\|\\+\\[0\\-9\\]$"
|
||||
end
|
||||
|
||||
test "over limit", %{site: site} do
|
||||
for i <- 1..maximum_page_rules() do
|
||||
assert {:ok, _} =
|
||||
add_page_rule(site, %{"page_path" => "/test/#{i}"})
|
||||
end
|
||||
|
||||
assert count_page_rules(site) == maximum_page_rules()
|
||||
|
||||
assert {:error, changeset} =
|
||||
add_page_rule(site, %{"page_path" => "/test/31"})
|
||||
|
||||
refute changeset.valid?
|
||||
assert changeset.errors == [page_path: {"maximum reached", []}]
|
||||
end
|
||||
|
||||
test "with added_by", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_page_rule(site, %{"page_path" => "/test"},
|
||||
added_by: build(:user, name: "Joe", email: "joe@example.com")
|
||||
)
|
||||
|
||||
assert rule.added_by == "Joe <joe@example.com>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "page pattern matching" do
|
||||
test "no wildcard", %{site: site} do
|
||||
assert {:ok, _} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
assert page_blocked?(site, "/test")
|
||||
refute page_blocked?(site, "/test/hello")
|
||||
refute page_blocked?(site, "test")
|
||||
end
|
||||
|
||||
test "wildcard", %{site: site} do
|
||||
assert {:ok, _} = add_page_rule(site, %{"page_path" => "/test/*"})
|
||||
refute page_blocked?(site, "/test")
|
||||
assert page_blocked?(site, "/test/")
|
||||
assert page_blocked?(site, "/test/hello")
|
||||
refute page_blocked?(site, "test")
|
||||
refute page_blocked?(site, "/testing")
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_page_rule/2" do
|
||||
test "is idempontent", %{site: site} do
|
||||
{:ok, rule} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
assert remove_page_rule(site, rule.id) == :ok
|
||||
refute Repo.get(Plausible.Shield.PageRule, rule.id)
|
||||
assert remove_page_rule(site, rule.id) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_page_rules/1" do
|
||||
test "empty", %{site: site} do
|
||||
assert(list_page_rules(site) == [])
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "many", %{site: site} do
|
||||
{:ok, r1} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
:timer.sleep(1000)
|
||||
{:ok, r2} = add_page_rule(site, %{"page_path" => "/test"})
|
||||
assert [^r2, ^r1] = list_page_rules(site)
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_page_rules/1" do
|
||||
test "counts", %{site: site} do
|
||||
assert count_page_rules(site) == 0
|
||||
{:ok, _} = add_page_rule(site, %{"page_path" => "/test1"})
|
||||
assert count_page_rules(site) == 1
|
||||
{:ok, _} = add_page_rule(site, %{"page_path" => "/test2"})
|
||||
assert count_page_rules(site) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "Page Rules" do
|
||||
test "end to end", %{site: site} do
|
||||
site2 = insert(:site)
|
||||
|
||||
assert count_page_rules(site.id) == 0
|
||||
assert list_page_rules(site.id) == []
|
||||
|
||||
assert {:ok, rule} =
|
||||
add_page_rule(site.id, %{
|
||||
"page_path" => "/test"
|
||||
})
|
||||
|
||||
add_page_rule(site2, %{"page_path" => "/test"})
|
||||
|
||||
assert count_page_rules(site) == 1
|
||||
assert [%{id: rule_id}] = list_page_rules(site)
|
||||
assert rule.id == rule_id
|
||||
assert rule.page_path == "/test"
|
||||
assert rule.action == :deny
|
||||
refute rule.from_cache?
|
||||
assert page_blocked?(site, "/test")
|
||||
refute page_blocked?(site, "/testing")
|
||||
end
|
||||
end
|
||||
end
|
@ -257,7 +257,7 @@ defmodule PlausibleWeb.SiteController do
|
||||
end
|
||||
|
||||
def settings_shields(conn, %{"shield" => shield})
|
||||
when shield in ["ip_addresses", "countries"] do
|
||||
when shield in ["ip_addresses", "countries", "pages"] do
|
||||
site = conn.assigns.site
|
||||
|
||||
conn
|
||||
|
304
lib/plausible_web/live/shields/page_rules.ex
Normal file
304
lib/plausible_web/live/shields/page_rules.ex
Normal file
@ -0,0 +1,304 @@
|
||||
defmodule PlausibleWeb.Live.Shields.PageRules do
|
||||
@moduledoc """
|
||||
LiveView allowing page Rules management
|
||||
"""
|
||||
|
||||
use Phoenix.LiveComponent, global_prefixes: ~w(x-)
|
||||
use Phoenix.HTML
|
||||
|
||||
alias PlausibleWeb.Live.Components.Modal
|
||||
alias Plausible.Shields
|
||||
alias Plausible.Shield
|
||||
|
||||
import PlausibleWeb.ErrorHelpers
|
||||
|
||||
def update(assigns, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
page_rules_count: assigns[:page_rules_count] || socket.assigns.page_rules_count,
|
||||
site: assigns[:site] || socket.assigns.site,
|
||||
current_user: assigns[:current_user] || socket.assigns.current_user,
|
||||
form: new_form()
|
||||
)
|
||||
|> assign_new(:page_rules, fn %{site: site} ->
|
||||
Shields.list_page_rules(site)
|
||||
end)
|
||||
|> assign_new(:redundant_rules, fn %{page_rules: page_rules} ->
|
||||
detect_redundancy(page_rules)
|
||||
end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<section class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden">
|
||||
<div class="py-6 px-4 sm:p-6">
|
||||
<header class="relative">
|
||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
|
||||
Pages Block List
|
||||
</h2>
|
||||
<p class="mt-1 mb-4 text-sm leading-5 text-gray-500 dark:text-gray-200">
|
||||
Reject incoming traffic for specific pages
|
||||
</p>
|
||||
|
||||
<PlausibleWeb.Components.Generic.docs_info slug="top-pages#block-traffic-from-specific-pages-or-sections" />
|
||||
</header>
|
||||
<div class="border-t border-gray-200 pt-4 grid">
|
||||
<div
|
||||
:if={@page_rules_count < Shields.maximum_page_rules()}
|
||||
class="mt-4 sm:ml-4 sm:mt-0 justify-self-end"
|
||||
>
|
||||
<PlausibleWeb.Components.Generic.button
|
||||
id="add-page-rule"
|
||||
x-data
|
||||
x-on:click={Modal.JS.open("page-rule-form-modal")}
|
||||
>
|
||||
+ Add Page
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
:if={@page_rules_count >= Shields.maximum_page_rules()}
|
||||
class="mt-4"
|
||||
title="Maximum number of pages reached"
|
||||
>
|
||||
<p>
|
||||
You've reached the maximum number of pages you can block (<%= Shields.maximum_page_rules() %>). Please remove one before adding another.
|
||||
</p>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</div>
|
||||
|
||||
<.live_component module={Modal} id="page-rule-form-modal">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@form}
|
||||
phx-submit="save-page-rule"
|
||||
phx-target={@myself}
|
||||
class="max-w-md w-full mx-auto bg-white dark:bg-gray-800"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100 mb-8">Add Page to Block List</h2>
|
||||
|
||||
<.live_component
|
||||
submit_name="page_rule[page_path]"
|
||||
submit_value={f[:page_path].value}
|
||||
display_value={f[:page_path].value || ""}
|
||||
module={PlausibleWeb.Live.Components.ComboBox}
|
||||
suggest_fun={fn input, options -> suggest_page_paths(input, options, @site) end}
|
||||
id={f[:page_path].id}
|
||||
creatable
|
||||
/>
|
||||
|
||||
<%= error_tag(f, :page_path) %>
|
||||
|
||||
<p class="text-sm mt-2 text-gray-500 dark:text-gray-200">
|
||||
You can use a wildcard (<code>*</code>) to match multiple pages. For example,
|
||||
<code>/blog/*</code>
|
||||
will match <code>/blog/post</code>.
|
||||
Once added, we will start rejecting traffic from this page within a few minutes.
|
||||
</p>
|
||||
<div class="py-4 mt-8">
|
||||
<PlausibleWeb.Components.Generic.button type="submit" class="w-full">
|
||||
Add Page →
|
||||
</PlausibleWeb.Components.Generic.button>
|
||||
</div>
|
||||
</.form>
|
||||
</.live_component>
|
||||
|
||||
<p
|
||||
:if={Enum.empty?(@page_rules)}
|
||||
class="text-sm text-gray-800 dark:text-gray-200 mt-12 mb-8 text-center"
|
||||
>
|
||||
No Page Rules configured for this Site.
|
||||
</p>
|
||||
<div
|
||||
:if={not Enum.empty?(@page_rules)}
|
||||
class="mt-8 overflow-hidden border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg"
|
||||
>
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
page
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
<span class="sr-only">Remove</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for rule <- @page_rules do %>
|
||||
<tr class="text-gray-900 dark:text-gray-100">
|
||||
<td class="px-6 py-4 text-sm font-medium max-w-xs truncate text-ellipsis overflow-hidden">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
id={"page-#{rule.id}"}
|
||||
class="mr-4 cursor-help border-b border-dotted border-gray-400 text-ellipsis overflow-hidden"
|
||||
title={"#{rule.page_path}\n\nAdded at #{format_added_at(rule.inserted_at, @site.timezone)} by #{rule.added_by}"}
|
||||
>
|
||||
<%= rule.page_path %>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
<div class="flex items-center">
|
||||
<span :if={rule.action == :deny}>
|
||||
Blocked
|
||||
</span>
|
||||
<span :if={rule.action == :allow}>
|
||||
Allowed
|
||||
</span>
|
||||
|
||||
<span
|
||||
:if={@redundant_rules[rule.id]}
|
||||
title={"This rule might be redundant because the following rules may match first:\n\n#{Enum.join(@redundant_rules[rule.id], "\n")}"}
|
||||
class="pl-4"
|
||||
>
|
||||
<Heroicons.exclamation_triangle class="h-4 w-4 text-red-500" />
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 text-sm font-medium text-right">
|
||||
<button
|
||||
id={"remove-page-rule-#{rule.id}"}
|
||||
phx-target={@myself}
|
||||
phx-click="remove-page-rule"
|
||||
phx-value-rule-id={rule.id}
|
||||
class="text-sm text-red-600"
|
||||
data-confirm="Are you sure you want to revoke this rule?"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("save-page-rule", %{"page_rule" => params}, socket) do
|
||||
user = socket.assigns.current_user
|
||||
|
||||
case Shields.add_page_rule(
|
||||
socket.assigns.site.id,
|
||||
params,
|
||||
added_by: user
|
||||
) do
|
||||
{:ok, rule} ->
|
||||
page_rules = [rule | socket.assigns.page_rules]
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> Modal.close("page-rule-form-modal")
|
||||
|> assign(
|
||||
form: new_form(),
|
||||
page_rules: page_rules,
|
||||
page_rules_count: socket.assigns.page_rules_count + 1,
|
||||
redundant_rules: detect_redundancy(page_rules)
|
||||
)
|
||||
|
||||
# Make sure to clear the combobox input after adding a page rule, on subsequent modal reopening
|
||||
send_update(PlausibleWeb.Live.Components.ComboBox,
|
||||
id: "page_rule_page_code",
|
||||
display_value: ""
|
||||
)
|
||||
|
||||
send_flash(
|
||||
:success,
|
||||
"Page rule added successfully. Traffic will be rejected within a few minutes."
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("remove-page-rule", %{"rule-id" => rule_id}, socket) do
|
||||
Shields.remove_page_rule(socket.assigns.site.id, rule_id)
|
||||
|
||||
send_flash(
|
||||
:success,
|
||||
"Page rule removed successfully. Traffic will be resumed within a few minutes."
|
||||
)
|
||||
|
||||
page_rules = Enum.reject(socket.assigns.page_rules, &(&1.id == rule_id))
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
page_rules_count: socket.assigns.page_rules_count - 1,
|
||||
page_rules: page_rules,
|
||||
redundant_rules: detect_redundancy(page_rules)
|
||||
)}
|
||||
end
|
||||
|
||||
def send_flash(kind, message) do
|
||||
send(self(), {:flash, kind, message})
|
||||
end
|
||||
|
||||
defp new_form() do
|
||||
%Shield.PageRule{}
|
||||
|> Shield.PageRule.changeset(%{})
|
||||
|> to_form()
|
||||
end
|
||||
|
||||
defp format_added_at(dt, tz) do
|
||||
dt
|
||||
|> Plausible.Timezones.to_datetime_in_timezone(tz)
|
||||
|> Timex.format!("{YYYY}-{0M}-{0D} {h24}:{m}:{s}")
|
||||
end
|
||||
|
||||
def suggest_page_paths(input, _options, site) do
|
||||
query = Plausible.Stats.Query.from(site, %{})
|
||||
|
||||
site
|
||||
|> Plausible.Stats.filter_suggestions(query, "page", input)
|
||||
|> Enum.map(fn %{label: label, value: value} -> {label, value} end)
|
||||
end
|
||||
|
||||
defp detect_redundancy(page_rules) do
|
||||
page_rules
|
||||
|> Enum.reduce(%{}, fn rule, acc ->
|
||||
{[^rule], remaining_rules} =
|
||||
Enum.split_with(
|
||||
page_rules,
|
||||
fn r -> r == rule end
|
||||
)
|
||||
|
||||
conflicting =
|
||||
remaining_rules
|
||||
|> Enum.filter(fn candidate ->
|
||||
rule
|
||||
|> Map.fetch!(:page_path_pattern)
|
||||
|> maybe_compile()
|
||||
|> Regex.match?(candidate.page_path)
|
||||
end)
|
||||
|> Enum.map(& &1.id)
|
||||
|
||||
Enum.reduce(conflicting, acc, fn conflicting_rule_id, acc ->
|
||||
Map.update(acc, conflicting_rule_id, [rule.page_path], fn existing ->
|
||||
[rule.page_path | existing]
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_compile(pattern) when is_binary(pattern), do: Regex.compile!(pattern)
|
||||
defp maybe_compile(%Regex{} = pattern), do: pattern
|
||||
end
|
53
lib/plausible_web/live/shields/pages.ex
Normal file
53
lib/plausible_web/live/shields/pages.ex
Normal file
@ -0,0 +1,53 @@
|
||||
defmodule PlausibleWeb.Live.Shields.Pages do
|
||||
@moduledoc """
|
||||
LiveView for IP Addresses Shield
|
||||
"""
|
||||
use PlausibleWeb, :live_view
|
||||
use Phoenix.HTML
|
||||
|
||||
alias Plausible.Shields
|
||||
alias Plausible.Sites
|
||||
|
||||
def mount(
|
||||
_params,
|
||||
%{
|
||||
"domain" => domain,
|
||||
"current_user_id" => user_id
|
||||
},
|
||||
socket
|
||||
) do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn ->
|
||||
Sites.get_for_user!(user_id, domain, [:owner, :admin, :super_admin])
|
||||
end)
|
||||
|> assign_new(:page_rules_count, fn %{site: site} ->
|
||||
Shields.count_page_rules(site)
|
||||
end)
|
||||
|> assign_new(:current_user, fn ->
|
||||
Plausible.Repo.get(Plausible.Auth.User, user_id)
|
||||
end)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.flash_messages flash={@flash} />
|
||||
<.live_component
|
||||
module={PlausibleWeb.Live.Shields.PageRules}
|
||||
current_user={@current_user}
|
||||
page_rules_count={@page_rules_count}
|
||||
site={@site}
|
||||
id="page-rules-#{@current_user.id}"
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_info({:flash, kind, message}, socket) do
|
||||
socket = put_live_flash(socket, kind, message)
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
@ -14,4 +14,11 @@
|
||||
"domain" => @site.domain
|
||||
}
|
||||
) %>
|
||||
<% "pages" -> %>
|
||||
<%= live_render(@conn, PlausibleWeb.Live.Shields.Pages,
|
||||
session: %{
|
||||
"site_id" => @site.id,
|
||||
"domain" => @site.domain
|
||||
}
|
||||
) %>
|
||||
<% end %>
|
||||
|
@ -65,10 +65,15 @@ defmodule PlausibleWeb.LayoutView do
|
||||
%{
|
||||
key: "Shields",
|
||||
icon: :shield_exclamation,
|
||||
value: [
|
||||
%{key: "IP Addresses", value: "shields/ip_addresses"},
|
||||
%{key: "Countries", value: "shields/countries"}
|
||||
]
|
||||
value:
|
||||
[
|
||||
%{key: "IP Addresses", value: "shields/ip_addresses"},
|
||||
%{key: "Countries", value: "shields/countries"},
|
||||
if FunWithFlags.enabled?(:shield_pages, for: conn.assigns[:site]) do
|
||||
%{key: "Pages", value: "shields/pages"}
|
||||
end
|
||||
]
|
||||
|> Enum.reject(&is_nil/1)
|
||||
},
|
||||
%{key: "Email Reports", value: "email-reports", icon: :envelope},
|
||||
if conn.assigns[:current_user_role] == :owner do
|
||||
|
@ -11,6 +11,7 @@
|
||||
# and so on) as they will fail if something goes wrong.
|
||||
|
||||
FunWithFlags.enable(:imports_exports)
|
||||
FunWithFlags.enable(:shield_pages)
|
||||
|
||||
user = Plausible.Factory.insert(:user, email: "user@plausible.test", password: "plausible")
|
||||
|
||||
|
@ -158,6 +158,25 @@ defmodule Plausible.Ingestion.EventTest do
|
||||
assert dropped.drop_reason == :site_country_blocklist
|
||||
end
|
||||
|
||||
test "event pipeline drops a request when page is on blocklist" do
|
||||
site = insert(:site)
|
||||
|
||||
payload = %{
|
||||
name: "pageview",
|
||||
url: "http://dummy.site/blocked/page",
|
||||
domain: site.domain
|
||||
}
|
||||
|
||||
conn = build_conn(:post, "/api/events", payload)
|
||||
|
||||
{:ok, _} = Plausible.Shields.add_page_rule(site, %{"page_path" => "/blocked/**"})
|
||||
|
||||
assert {:ok, request} = Request.build(conn)
|
||||
|
||||
assert {:ok, %{buffered: [], dropped: [dropped]}} = Event.build_and_buffer(request)
|
||||
assert dropped.drop_reason == :site_page_blocklist
|
||||
end
|
||||
|
||||
test "event pipeline drops events for site with accept_trafic_until in the past" do
|
||||
yesterday = Date.add(Date.utc_today(), -1)
|
||||
|
||||
|
107
test/plausible/shield/page_rule_cache_test.exs
Normal file
107
test/plausible/shield/page_rule_cache_test.exs
Normal file
@ -0,0 +1,107 @@
|
||||
defmodule Plausible.Shield.PageRuleCacheTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
alias Plausible.Shield.PageRule
|
||||
alias Plausible.Shield.PageRuleCache
|
||||
alias Plausible.Shields
|
||||
|
||||
describe "public cache interface" do
|
||||
test "cache caches page rules", %{test: test} do
|
||||
cache_opts = [force?: true, cache_name: test]
|
||||
|
||||
{:ok, _} =
|
||||
Supervisor.start_link(
|
||||
[
|
||||
{PageRuleCache,
|
||||
[cache_name: test, child_id: :page_rules_cache_id, ets_options: [:bag]]}
|
||||
],
|
||||
strategy: :one_for_one,
|
||||
name: :"cache_supervisor_#{test}"
|
||||
)
|
||||
|
||||
site1 = insert(:site)
|
||||
site2 = insert(:site)
|
||||
|
||||
{:ok, %{id: rid1}} = Shields.add_page_rule(site1, %{"page_path" => "/test/1"})
|
||||
{:ok, %{id: rid2}} = Shields.add_page_rule(site1, %{"page_path" => "/test/2"})
|
||||
{:ok, %{id: rid3}} = Shields.add_page_rule(site2, %{"page_path" => "/test/2"})
|
||||
|
||||
:ok = PageRuleCache.refresh_all(cache_name: test)
|
||||
|
||||
:ok = Shields.remove_page_rule(site1, rid1)
|
||||
|
||||
assert PageRuleCache.size(test) == 3
|
||||
|
||||
# the rule order should be deterministic, but with 1s timestamp sorting precision
|
||||
# race conditions may happen during tests
|
||||
assert rules = PageRuleCache.get(site1.domain, cache_opts)
|
||||
rule_ids = Enum.map(rules, & &1.id)
|
||||
assert rid1 in rule_ids
|
||||
assert rid2 in rule_ids
|
||||
assert length(rules) == 2
|
||||
|
||||
assert %PageRule{from_cache?: true, id: ^rid3} =
|
||||
PageRuleCache.get(site2.domain, cache_opts)
|
||||
|
||||
refute PageRuleCache.get("rogue.example.com", cache_opts)
|
||||
end
|
||||
|
||||
test "page path patterns are already compiled when fetched from cache", %{test: test} do
|
||||
site = insert(:site)
|
||||
|
||||
{:ok, _} = start_test_cache(test)
|
||||
cache_opts = [force?: true, cache_name: test]
|
||||
|
||||
{:ok, _} = Shields.add_page_rule(site, %{"page_path" => "/hello/**/world"})
|
||||
:ok = PageRuleCache.refresh_all(cache_name: test)
|
||||
assert regex = PageRuleCache.get(site.domain, cache_opts).page_path_pattern
|
||||
assert regex == ~r/^\/hello\/.*\/world$/
|
||||
end
|
||||
|
||||
test "cache allows lookups for page paths on sites with changed domain", %{test: test} do
|
||||
{:ok, _} = start_test_cache(test)
|
||||
cache_opts = [force?: true, cache_name: test]
|
||||
site = insert(:site, domain: "new.example.com", domain_changed_from: "old.example.com")
|
||||
|
||||
{:ok, _} = Shields.add_page_rule(site, %{"page_path" => "/#{test}"})
|
||||
:ok = PageRuleCache.refresh_all(cache_name: test)
|
||||
|
||||
assert PageRuleCache.get("old.example.com", cache_opts)
|
||||
assert PageRuleCache.get("new.example.com", cache_opts)
|
||||
refute PageRuleCache.get("rogue.example.com", cache_opts)
|
||||
end
|
||||
|
||||
test "refreshes only recently added pages rules", %{test: test} do
|
||||
{:ok, _} = start_test_cache(test)
|
||||
|
||||
domain = "site1.example.com"
|
||||
site = insert(:site, domain: domain)
|
||||
|
||||
cache_opts = [cache_name: test, force?: true]
|
||||
|
||||
yesterday =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(-1 * 60 * 60 * 24)
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
{:ok, r1} = Plausible.Shields.add_page_rule(site, %{"page_path" => "/test/1"})
|
||||
Ecto.Changeset.change(r1, inserted_at: yesterday, updated_at: yesterday) |> Repo.update!()
|
||||
{:ok, _} = Plausible.Shields.add_page_rule(site, %{"page_path" => "/test/2"})
|
||||
|
||||
assert PageRuleCache.get(domain, cache_opts) == nil
|
||||
|
||||
assert :ok = PageRuleCache.refresh_updated_recently(cache_opts)
|
||||
|
||||
assert %{page_path_pattern: ~r[^/test/2$]} = PageRuleCache.get(domain, cache_opts)
|
||||
|
||||
assert :ok = PageRuleCache.refresh_all(cache_opts)
|
||||
|
||||
assert [_, _] = PageRuleCache.get(domain, cache_opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp start_test_cache(cache_name) do
|
||||
%{start: {m, f, a}} = PageRuleCache.child_spec(cache_name: cache_name, ets_options: [:bag])
|
||||
apply(m, f, a)
|
||||
end
|
||||
end
|
@ -1,270 +0,0 @@
|
||||
defmodule Plausible.ShieldsTest do
|
||||
use Plausible.DataCase
|
||||
import Plausible.Shields
|
||||
|
||||
setup do
|
||||
site = insert(:site)
|
||||
{:ok, %{site: site}}
|
||||
end
|
||||
|
||||
describe "add_ip_rule/2" do
|
||||
test "no input", %{site: site} do
|
||||
assert {:error, changeset} = add_ip_rule(site, %{})
|
||||
assert changeset.errors == [inet: {"can't be blank", [validation: :required]}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "unsupported netmask", %{site: site} do
|
||||
assert {:error, changeset} = add_ip_rule(site, %{"inet" => "127.0.0.0/24"})
|
||||
assert changeset.errors == [inet: {"netmask unsupported", []}]
|
||||
refute changeset.valid?
|
||||
assert {:ok, _} = add_ip_rule(site, %{"inet" => "127.0.0.0/32"})
|
||||
end
|
||||
|
||||
test "incorrect ip", %{site: site} do
|
||||
assert {:error, changeset} = add_ip_rule(site, %{"inet" => "999.999.999.999"})
|
||||
|
||||
assert changeset.errors == [
|
||||
inet: {"is invalid", [{:type, EctoNetwork.INET}, {:validation, :cast}]}
|
||||
]
|
||||
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "non-strict IPs", %{site: site} do
|
||||
assert {:error, _} = add_ip_rule(site, %{"inet" => "111"})
|
||||
end
|
||||
|
||||
test "double insert", %{site: site} do
|
||||
assert {:ok, _} = add_ip_rule(site, %{"inet" => "0.0.0.111"})
|
||||
assert {:error, changeset} = add_ip_rule(site, %{"inet" => "0.0.0.111"})
|
||||
refute changeset.valid?
|
||||
|
||||
assert changeset.errors == [
|
||||
inet:
|
||||
{"has already been taken",
|
||||
[
|
||||
{:constraint, :unique},
|
||||
{:constraint_name, "shield_rules_ip_site_id_inet_index"}
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
||||
test "ipv6", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site, %{"inet" => "2001:0000:130F:0000:0000:09C0:876A:130B"})
|
||||
|
||||
assert ^rule = Repo.get(Plausible.Shield.IPRule, rule.id)
|
||||
end
|
||||
|
||||
test "ipv4", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.1"})
|
||||
|
||||
assert ^rule = Repo.get(Plausible.Shield.IPRule, rule.id)
|
||||
end
|
||||
|
||||
test "over limit", %{site: site} do
|
||||
for i <- 1..maximum_ip_rules() do
|
||||
assert {:ok, _} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.#{i}"})
|
||||
end
|
||||
|
||||
assert count_ip_rules(site) == maximum_ip_rules()
|
||||
|
||||
assert {:error, changeset} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.31"})
|
||||
|
||||
refute changeset.valid?
|
||||
assert changeset.errors == [inet: {"maximum reached", []}]
|
||||
end
|
||||
|
||||
test "with added_by", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site, %{"inet" => "1.1.1.1"},
|
||||
added_by: build(:user, name: "Joe", email: "joe@example.com")
|
||||
)
|
||||
|
||||
assert rule.added_by == "Joe <joe@example.com>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_ip_rule/2" do
|
||||
test "is idempontent", %{site: site} do
|
||||
{:ok, rule} = add_ip_rule(site, %{"inet" => "127.0.0.1"})
|
||||
assert remove_ip_rule(site, rule.id) == :ok
|
||||
refute Repo.get(Plausible.Shield.IPRule, rule.id)
|
||||
assert remove_ip_rule(site, rule.id) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_ip_rules/1" do
|
||||
test "empty", %{site: site} do
|
||||
assert(list_ip_rules(site) == [])
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "many", %{site: site} do
|
||||
{:ok, r1} = add_ip_rule(site, %{"inet" => "127.0.0.1"})
|
||||
:timer.sleep(1000)
|
||||
{:ok, r2} = add_ip_rule(site, %{"inet" => "127.0.0.2"})
|
||||
assert [^r2, ^r1] = list_ip_rules(site)
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_ip_rules/1" do
|
||||
test "counts", %{site: site} do
|
||||
assert count_ip_rules(site) == 0
|
||||
{:ok, _} = add_ip_rule(site, %{"inet" => "127.0.0.1"})
|
||||
assert count_ip_rules(site) == 1
|
||||
{:ok, _} = add_ip_rule(site, %{"inet" => "127.0.0.2"})
|
||||
assert count_ip_rules(site) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "IP Rules" do
|
||||
test "end to end", %{site: site} do
|
||||
site2 = insert(:site)
|
||||
|
||||
assert count_ip_rules(site.id) == 0
|
||||
assert list_ip_rules(site.id) == []
|
||||
|
||||
assert {:ok, rule} =
|
||||
add_ip_rule(site.id, %{"inet" => "127.0.0.1", "description" => "Localhost"})
|
||||
|
||||
add_ip_rule(site2, %{"inet" => "127.0.0.1", "description" => "Localhost"})
|
||||
|
||||
assert count_ip_rules(site) == 1
|
||||
assert [^rule] = list_ip_rules(site)
|
||||
assert rule.inet == %Postgrex.INET{address: {127, 0, 0, 1}, netmask: 32}
|
||||
assert rule.description == "Localhost"
|
||||
assert rule.action == :deny
|
||||
refute rule.from_cache?
|
||||
end
|
||||
end
|
||||
|
||||
describe "add_country_rule/2" do
|
||||
test "no input", %{site: site} do
|
||||
assert {:error, changeset} = add_country_rule(site, %{})
|
||||
assert changeset.errors == [country_code: {"can't be blank", [validation: :required]}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "unsupported country", %{site: site} do
|
||||
assert {:error, changeset} = add_country_rule(site, %{"country_code" => "0X"})
|
||||
assert changeset.errors == [country_code: {"is invalid", []}]
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "incorrect country format", %{site: site} do
|
||||
assert {:error, changeset} = add_country_rule(site, %{"country_code" => "Germany"})
|
||||
|
||||
assert changeset.errors ==
|
||||
[
|
||||
{:country_code, {"is invalid", []}},
|
||||
{:country_code,
|
||||
{"should be %{count} character(s)",
|
||||
[count: 2, validation: :length, kind: :is, type: :string]}}
|
||||
]
|
||||
|
||||
refute changeset.valid?
|
||||
end
|
||||
|
||||
test "double insert", %{site: site} do
|
||||
assert {:ok, _} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
assert {:error, changeset} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
refute changeset.valid?
|
||||
|
||||
assert changeset.errors == [
|
||||
country_code:
|
||||
{"has already been taken",
|
||||
[
|
||||
{:constraint, :unique},
|
||||
{:constraint_name, "shield_rules_country_site_id_country_code_index"}
|
||||
]}
|
||||
]
|
||||
end
|
||||
|
||||
test "over limit", %{site: site} do
|
||||
country_codes =
|
||||
Location.Country.all()
|
||||
|> Enum.take(Plausible.Shields.maximum_country_rules())
|
||||
|> Enum.map(& &1.alpha_2)
|
||||
|
||||
for cc <- country_codes do
|
||||
assert {:ok, _} =
|
||||
add_country_rule(site, %{"country_code" => cc})
|
||||
end
|
||||
|
||||
assert count_country_rules(site) == maximum_country_rules()
|
||||
|
||||
assert {:error, changeset} =
|
||||
add_country_rule(site, %{"country_code" => "US"})
|
||||
|
||||
refute changeset.valid?
|
||||
assert changeset.errors == [country_code: {"maximum reached", []}]
|
||||
end
|
||||
|
||||
test "with added_by", %{site: site} do
|
||||
assert {:ok, rule} =
|
||||
add_country_rule(site, %{"country_code" => "EE"},
|
||||
added_by: build(:user, name: "Joe", email: "joe@example.com")
|
||||
)
|
||||
|
||||
assert rule.added_by == "Joe <joe@example.com>"
|
||||
end
|
||||
end
|
||||
|
||||
describe "remove_country_rule/2" do
|
||||
test "is idempontent", %{site: site} do
|
||||
{:ok, rule} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
assert remove_country_rule(site, rule.id) == :ok
|
||||
refute Repo.get(Plausible.Shield.CountryRule, rule.id)
|
||||
assert remove_country_rule(site, rule.id) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_country_rules/1" do
|
||||
test "empty", %{site: site} do
|
||||
assert(list_country_rules(site) == [])
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
test "many", %{site: site} do
|
||||
{:ok, r1} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
:timer.sleep(1000)
|
||||
{:ok, r2} = add_country_rule(site, %{"country_code" => "PL"})
|
||||
assert [^r2, ^r1] = list_country_rules(site)
|
||||
end
|
||||
end
|
||||
|
||||
describe "count_country_rules/1" do
|
||||
test "counts", %{site: site} do
|
||||
assert count_country_rules(site) == 0
|
||||
{:ok, _} = add_country_rule(site, %{"country_code" => "EE"})
|
||||
assert count_country_rules(site) == 1
|
||||
{:ok, _} = add_country_rule(site, %{"country_code" => "PL"})
|
||||
assert count_country_rules(site) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "Country Rules" do
|
||||
test "end to end", %{site: site} do
|
||||
site2 = insert(:site)
|
||||
|
||||
assert count_country_rules(site.id) == 0
|
||||
assert list_country_rules(site.id) == []
|
||||
|
||||
assert {:ok, rule} =
|
||||
add_country_rule(site.id, %{"country_code" => "EE"})
|
||||
|
||||
add_country_rule(site2, %{"country_code" => "PL"})
|
||||
|
||||
assert count_country_rules(site) == 1
|
||||
assert [^rule] = list_country_rules(site)
|
||||
assert rule.country_code == "EE"
|
||||
assert rule.action == :deny
|
||||
refute rule.from_cache?
|
||||
end
|
||||
end
|
||||
end
|
@ -95,7 +95,7 @@ defmodule PlausibleWeb.Live.Shields.IPAddressesTest do
|
||||
assert element_exists?(html, ~s/a[phx-click="prefill-own-ip-rule"]/)
|
||||
end
|
||||
|
||||
test "form modal does not contains link to add own IP if already added", %{
|
||||
test "form modal does not contain link to add own IP if already added", %{
|
||||
site: site,
|
||||
conn: conn
|
||||
} do
|
||||
|
176
test/plausible_web/live/shields/pages_test.exs
Normal file
176
test/plausible_web/live/shields/pages_test.exs
Normal file
@ -0,0 +1,176 @@
|
||||
defmodule PlausibleWeb.Live.Shields.PagesTest do
|
||||
use PlausibleWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Plausible.Test.Support.HTML
|
||||
|
||||
alias Plausible.Shields
|
||||
|
||||
setup [:create_user, :create_site, :log_in]
|
||||
|
||||
describe "Page Rules - static" do
|
||||
test "renders page rules page with empty list", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/pages")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "No Page Rules configured for this Site"
|
||||
assert resp =~ "Pages Block List"
|
||||
end
|
||||
|
||||
test "lists page rules with remove actions", %{conn: conn, site: site} do
|
||||
{:ok, r1} =
|
||||
Shields.add_page_rule(site, %{"page_path" => "/test/1"})
|
||||
|
||||
{:ok, r2} =
|
||||
Shields.add_page_rule(site, %{"page_path" => "/test/2"})
|
||||
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/pages")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert resp =~ "/test/1"
|
||||
assert resp =~ "/test/2"
|
||||
|
||||
assert remove_button_1 = find(resp, "#remove-page-rule-#{r1.id}")
|
||||
assert remove_button_2 = find(resp, "#remove-page-rule-#{r2.id}")
|
||||
|
||||
assert text_of_attr(remove_button_1, "phx-click" == "remove-page-rule")
|
||||
assert text_of_attr(remove_button_1, "phx-value-rule-id" == r1.id)
|
||||
assert text_of_attr(remove_button_2, "phx-click" == "remove-page-rule")
|
||||
assert text_of_attr(remove_button_2, "phx-value-rule-id" == r2.id)
|
||||
end
|
||||
|
||||
test "add rule button is rendered", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/pages")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
assert element_exists?(resp, ~s/button#add-page-rule[x-data]/)
|
||||
attr = text_of_attr(resp, ~s/button#add-page-rule/, "x-on:click")
|
||||
|
||||
assert attr =~ "open-modal"
|
||||
assert attr =~ "page-rule-form-modal"
|
||||
end
|
||||
|
||||
test "add rule button is not rendered when maximum reached", %{conn: conn, site: site} do
|
||||
for i <- 1..Shields.maximum_page_rules() do
|
||||
assert {:ok, _} =
|
||||
Shields.add_page_rule(site, %{"page_path" => "/test/#{i}"})
|
||||
end
|
||||
|
||||
conn = get(conn, "/#{site.domain}/settings/shields/pages")
|
||||
resp = html_response(conn, 200)
|
||||
|
||||
refute element_exists?(resp, ~s/button#add-page-rule[x-data]/)
|
||||
assert resp =~ "Maximum number of pages reached"
|
||||
assert resp =~ "You've reached the maximum number of pages you can block (30)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Page Rules - LiveView" do
|
||||
test "modal contains form", %{site: site, conn: conn} do
|
||||
lv = get_liveview(conn, site)
|
||||
html = render(lv)
|
||||
|
||||
assert element_exists?(
|
||||
html,
|
||||
~s/form[phx-submit="save-page-rule"] input[name="page_rule\[page_path\]"]/
|
||||
)
|
||||
|
||||
assert submit_button(html, ~s/form[phx-submit="save-page-rule"]/)
|
||||
end
|
||||
|
||||
test "submitting a valid Page saves it", %{conn: conn, site: site} do
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{
|
||||
"page_rule[page_path]" => "/test/**"
|
||||
})
|
||||
|
||||
html = render(lv)
|
||||
|
||||
assert html =~ "/test/**"
|
||||
|
||||
assert [%{page_path: "/test/**"}] = Shields.list_page_rules(site)
|
||||
end
|
||||
|
||||
test "submitting invalid Page renders error", %{conn: conn, site: site} do
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{
|
||||
"page_rule[page_path]" => "WRONG"
|
||||
})
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "must start with /"
|
||||
end
|
||||
|
||||
test "clicking Remove deletes the rule", %{conn: conn, site: site} do
|
||||
{:ok, _} =
|
||||
Shields.add_page_rule(site, %{"page_path" => "/test/*/page"})
|
||||
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
html = render(lv)
|
||||
assert html =~ "/test/*/page"
|
||||
|
||||
lv |> element(~s/button[phx-click="remove-page-rule"]/) |> render_click()
|
||||
|
||||
html = render(lv)
|
||||
refute html =~ "/test/*/page"
|
||||
|
||||
assert Shields.count_page_rules(site) == 0
|
||||
end
|
||||
|
||||
test "conclicting rules are annotated with a warning", %{conn: conn, site: site} do
|
||||
lv = get_liveview(conn, site)
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{
|
||||
"page_rule[page_path]" => "/test/*"
|
||||
})
|
||||
|
||||
html = render(lv)
|
||||
refute html =~ "This rule might be redundant"
|
||||
|
||||
lv
|
||||
|> element("form")
|
||||
|> render_submit(%{
|
||||
"page_rule[page_path]" => "/test/another"
|
||||
})
|
||||
|
||||
html = render(lv)
|
||||
|
||||
assert html =~ "/test/*"
|
||||
assert html =~ "/test/another"
|
||||
|
||||
assert html =~
|
||||
"This rule might be redundant because the following rules may match first:\n\n/test/*"
|
||||
|
||||
broader_rule_id =
|
||||
site
|
||||
|> Shields.list_page_rules()
|
||||
|> Enum.find(&(&1.page_path == "/test/*"))
|
||||
|> Map.fetch!(:id)
|
||||
|
||||
lv |> element(~s/button#remove-page-rule-#{broader_rule_id}/) |> render_click()
|
||||
html = render(lv)
|
||||
|
||||
assert html =~ "/test/another"
|
||||
refute html =~ "/test/*"
|
||||
|
||||
refute html =~
|
||||
"This rule might be redundant because the following rules may match first:\n\n/test/*"
|
||||
end
|
||||
|
||||
defp get_liveview(conn, site) do
|
||||
conn = assign(conn, :live_module, PlausibleWeb.Live.Shields)
|
||||
{:ok, lv, _html} = live(conn, "/#{site.domain}/settings/shields/pages")
|
||||
|
||||
lv
|
||||
end
|
||||
end
|
||||
end
|
@ -3,6 +3,7 @@ Mox.defmock(Plausible.HTTPClient.Mock, for: Plausible.HTTPClient.Interface)
|
||||
Application.ensure_all_started(:double)
|
||||
FunWithFlags.enable(:window_time_on_page)
|
||||
FunWithFlags.enable(:imports_exports)
|
||||
FunWithFlags.enable(:shield_pages)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual)
|
||||
|
||||
if Mix.env() == :small_test do
|
||||
|
Loading…
Reference in New Issue
Block a user